Are your mixins ECMAScript 5 compatible?

I was working with a client recently on a project that could make full use of ECMAScript 5 when I came across an interesting problem. The issue stemmed from the use of mixins, a very common pattern in JavaScript where one object is assigned properties (including methods) from another. Most mixin functions look something like this:

function mixin(receiver, supplier) {
    for (var property in supplier) {
        if (supplier.hasOwnProperty(property)) {
            receiver[property] = supplier[property];
        }
    }
}

Inside of the mixin() function, a for loop iterates over all own properties of the supplier and assigns the value to the property of the same name on the receiver. Almost every JavaScript library has some form of this function, allowing you to write code like this:

mixin(object, {

    name: "Nicholas",

    sayName: function() {
        console.log(this.name);
    }

});

object.sayName();       // outputs "Nicholas"

In this example, object receives both the property name and the method sayName(). This was fine in ECMAScript 3 but doesn’t cover all the bases in ECMAScript 5.

The problem I ran into was with this pattern:

(function() {

    // to be filled in later
    var name;

    mixin(object, {

        get name() {
            return name;
        }

    });

    // let's just say this is later
    name = "Nicholas";

}());

console.log(object.name);       // undefined

This example looks a little bit contrived, but is an accurate depiction of the problem. The properties to be mixed in include an ECMAScript 5 accessor property with only a getter. That getter references a local variable called name that isn’t initialized to a variable and so receives the value of undefined. Later on, name is assigned a value so that the accessor can return a valid value. Unfortunately, object.name (the mixed-in property) always returns undefined. What’s going on here?

Look closer at the mixin() function. The loop is not, in fact, reassign properties from one object to another. It’s actually creating a data property with a given name and assigning it the returned by accessing that property on the supplier. For this example, mixin() effectively does this:

receiver.name = supplier.name;

The data property receiver.name is created and assigned the value of supplier.name. Of course, supplier.name has a getter that returns the value of the local name variable. At that point in time, name has a value of undefined, so that is the value stored in receiver.name. No getter is every created for receiver.name so the value never changes.

To fix this problem, you need to use property descriptors to properly mix properties from one object onto another. A pure ECMAScript 5 version of mixin() would be:

function mixin(receiver, supplier) {

    Object.keys(supplier).forEach(function(property) {
        Object.defineProperty(receiver, property, Object.getOwnPropertyDescriptor(supplier, property));
    });
}

In this new version of the function, Object.keys() is used to retrieve an array of all enumerable properties on supplier. Then, the forEach() method is used to iterate over those properties. The call to Object.getOwnPropertyDescriptor() retrieves the descriptor for each property of supplier. Since the descriptor contains all of the relevant information about the property, including getters and setters, that descriptor can be passed directly into Object.defineProperty() to create the same property on receiver. Using this new version of mixin(), the problematic pattern from earlier in this post works as you would expect. The getter is correctly being transferred to receiver from supplier.

Of course, if you still need to support older browsers then you’ll need a function that falls back to the ECMAScript 3 way:

function mixin(receiver, supplier) {
    if (Object.keys) {
        Object.keys(supplier).forEach(function(property) {
            Object.defineProperty(receiver, property, Object.getOwnPropertyDescriptor(supplier, property));
        });
    } else {
        for (var property in supplier) {
            if (supplier.hasOwnProperty(property)) {
                receiver[property] = supplier[property];
            }
        }
    }
}

If you’re using a mixin() function, be sure to double check that it works with ECMAScript 5, and specifically with getters and setters. Otherwise, you could find yourself running into errors like I did.

Update (12-December-2012): Fixed coding error.

Comments

  1. Andreas Göbel

    Nice article Nicholas.

    You probably want to replace Object.keys() with Object.getOwnPropertyNames() to also fetch the non-enumerable properties of a source object.

    Also I'd always rather use an ES5Shim library and just use the ES5 methods, instead explicitly create a fallback here.

  2. Johnathan Hebert

    Awesome stuff... thanks!

  3. herby

    The method is called `Object.defineProperty` (no `Own` there).

  4. Rick Waldron

    Heads up! "defineOwnProperty" => "defineProperty"

  5. Nicholas C. Zakas

    @Andreas - No I don't. :) Mixins only copy over the enumerable properties, so my use of Object.keys() is intentional. And if you've read this blog for a while, you know I'm not a fan of shims.

  6. Nicholas C. Zakas

    @Rick & Herby - thanks, fixed.

  7. Andrea Giammarchi

    redefining getters ... you gonna have bad time with your example too because all objects using that mixin will overwrite the name private variable for all others too.

    Getters with private scope access are a weird pattern "you" must understand before you can suggest everyone to do not worry about mixins 'cause that method gonna take care of all cases ;-)

    It's like shared properties in a prototype such array or objects, apparently one of the most confusing thing ever out there.

    Last, but not least, you are assuming that if Object.keys is there, Object.getOwnPropertyDescriptor is there too. I would rather check the latter one instead of keys because Object.keys is way too easy to shim so no guarantee the second method is available.

    After all this, nice post as usual.

  8. Kris Kowal

    I would recommend feature testing Object.defineOwnProperty rather than Object.keys. The latter can be faithfully shimmed so its presence is less likely to be a accurate indication of an ES5 compatible engine.

  9. Nicholas C. Zakas

    @Andrea - I'm aware that the variable will be shared by all instances, and that's precisely what I want. I'm not sure why you'd assume I didn't know that. And I never use shims, so I don't worry about people shimming out this functionality. If you do, then you've voided your warranty and all the trouble you get is deserved.

  10. Nicholas C. Zakas

    @Kris - As I mentioned to Andrea, shim at your own risk. I don't use shims and don't ever recommend other people do.

  11. Andrea Giammarchi

    P.S for getters, I mean properties with getters and setters defined through private scope ... this is a pattern that today JS developers are not use to so it would be nice if you could explain/show more on this otherwise this mixin function will cause many headaches ;-)

    Also, if the property was already defined there should be a check or a try catch plus Object.getOwnPropertyDescriptor would be even better option.

    What I mean is that you consider get/set but you don't want to consider enumerable:false ? This is another ES5 feature you should consider, imho :-)

  12. Nicholas C. Zakas

    @Andrea - I'm not trying to cover every possible aspect of this, just that the mixin functions that most people are using won't quite work with ECMAScript 5 objects the way that they intend. I'm not worried about all of the edge cases that you mention, as these are things that aren't always included in mixin functions. I'm also not concerned with non-enumerable properties, because mixins are designed to work only with enumerable properties. I'm assuming that if you want to use non-enumerable properties then you know what you're doing and also know how to modify this function appropriately.

  13. Andrea Giammarchi

    @Nicolas, shims don't work any different from partial implementations.

    Take Object.defineProperty in IE8, as example, is there and it does not work.

    Same could be for Object.keys, it might be there but not working or, since it's simple to shim and does not affect all objects, it could be implemented because widely used while getOwnPropertyDescriptor cannot be possibly shimmed in a meaningful way except returning always {enumerable:true,configurable:true,writable:true} doing eventually some check via delete obj[prop] for the configurable part.

    Last, when I wrote "you" I did not mean "you" ... I am pretty sure you know what you are doing there, your readers might not consider that get/set in a private scope might lead to problematic/unexpected results.

  14. Andrea Giammarchi

    sorry, last one, for Object.getOwnPropertyDescriptor I meant Object.getOwnPropertyNames VS Object.keys, apologies, now I am done :-)

  15. Andrea Giammarchi

    @Nicolas a mixin more than a class could use not enumerable properties and specially for getters/setters to store in a non enumerable property the value, i.e.


    var MixIt = Object.defineProperties({}, {
    name: {
    enumerable: true,
    get: function () {
    // notify or do stuff
    return this._name;
    },
    set: function (_name) {
    // notify or do stuff
    this._name = _name;
    }
    },
    _name: {
    writable: true
    }
    });

    I believe above example is a good ES5 one and makes indeed that name property portable as mixin rather than property to copy

  16. David Bruant

    I love to see the use of modern features.

    A couple of things:
    * The property name is missing in your Object.defineProperty calls.

    * I would feature-test on Object.defineProperty or inline getters, because the core of the problem is not "can I enumerate own properties?", but rather "what is the behavior when there are getter/setters in the environment?".
    True that browsers supporting getter/setters are the same than the ones supporting Object.keys, but it makes the code easier to read.

    * It's a nit, but I would put the feature-test above instead of inside

    var mixin;
    if (Object.defineProperty) {
    mixin = function mixin(receiver, supplier) {/*modern version*/}
    else{
    mixin = function mixin(receiver, supplier) {/*ES3 version*/}
    }

    The result of the test won't change over time.

  17. kc

    Dislike the approach of add more 'static' methods to built-in objects.

    How about a new built-in module 'meta' as a namespace for the object manipulation powertools:
    meta.mixin(myobj, mymixinobj);

    Maybe classes should be part of a high level sourcemap language - like TypeScript - which use the meta api tools.

  18. Nicholas C. Zakas

    @David - oops, thanks! I'm always shocked at how frequently I forget the property name when using defineProperty(). It literally happens to me every single time. I do like your approach, and would probably use that in library code. I thought the way I presented it would be more accessible to readers.

  19. Nicholas C. Zakas

    @kc - Don't read too much into this function as a global. The blog post was designed to point out a problem in current mixin code, and to provide a blueprint for how to solve it. I wasn't going to complicate things by creating a new global just to add one method to for this post. Of course, I hope whoever uses this kind of approach will attach the function to the correct owner object.

  20. Asen Bozhilov

    I want to ask, why you want to clone the getter?

    In my opinion the getter is used to encapsulate the algorithm of the value or to provide a public API for private property. In both cases it supplies the value.
    The idea of mixin method is to mix the property values of obj X in obj Y. In that sense I would like to use the ES3 version.

    In ES6 you could see 15.2.3.15 Object.assign ( target, source ). In the To Do list there is: "Invoke [[Get]] on property list derived from source, for each property in list [[Put]] on target"
    This would work exactly as the mixin in most JS libs.

  21. rosamez

    Why test for Object.keys, when you said your project will use es5? Is there a browser where you have getters/setters but no Object.keys? Why not adapt your mixin to your es5 code in this case?

    Also note that while you disregard shims, you still implement a shim (albeit locally) - which still may be a slippery slope (see comment above about Object.defineProperty and beloved IE8).

  22. Domenic Denicola

    +1 for copying non-enumerable properties. For example, all methods on built-ins (and possibly on ES6 classes; that's still being debated IIRC) are non-enumerable.

  23. David Bruant

    @Nicholas your code still doesn't work I think:

    Object.keys(supplier).forEach(function(value, property) {
    Object.defineProperty(receiver, property, Object.getOwnPropertyDescriptor(supplier, property));
    });

    The .forEach (and all array extras) function argument has the following signature: function(e, i, a) for element-index-array. I don't understand your use of two "value" and "properties" arguments. I would use only the first argument and call it something like "propName" (well, "property" is fine, I guess) and use this first argument in the defineProperty call (currently you're using the second argument).

    So, something like:

    Object.keys(supplier).forEach(function(property) {
    Object.defineProperty(receiver, property, Object.getOwnPropertyDescriptor(supplier, property));
    });

    Your point about making the content accessible to readers is interesting. I'm unsolved as to what's better in the context of a blog post. I think I'd try my more "cryptic" and wait for people to ask questions in the comments or on Twitter if they don't understand, but I understand your approach.

  24. Andreas Göbel

    @Nicholas:

    You mentioned several times in the comments that "mixins are designed to work only with enumerable properties". Is that your very own definition ?

    So far I always also wanted to copy/clone non-enumerables in my mixins, so I'm wondering if there is any best-practice or 'nono' I want to know about.

  25. Nicholas C. Zakas

    @Andreas - it's not a "nono" per se, but copying non-enumerables was problematic prior to ECMAScript 3 because there was no way to programmatically discover them. You would have to manually specify the properties to copy, so copying over enumerable properties was the way that most people did it. In ECMAScript 5 you can use Object.getOwnPropertyNames() to retrieve both enumerable and non-enumerable properties, so it's a bit easier if you'd like to do that. I've just not seen it done all that much.

  26. Nicholas C. Zakas

    @David - right you are, sir. I've fixed the code. Thanks.

  27. Nicholas C. Zakas

    @Domenic - yup, and that's why mixins usually limit themselves to enumerable properties. Remember, before ECMAScript 5, there was no easy way to get non-enumerable properties.

  28. Nicholas C. Zakas

    @rosamez - that's a good point, I was trying to build up the resulting function step-by-step for the reader, to make it easier to follow. But you're right, if you're only using ECMAScript 5, there's no reason to feature test. I'm not sure about your second point - I'm not implementing any shims here.

  29. Ghasem Kiani

    Since JavaScript is such a dynamic and “liberal” language, there are naturally various ways to use its functionalities. That said, IMHO, there are two types of mixins and they have two different use cases. The first type copies property descriptors from an object to another. This type can be used to “extend” an object. In this form of mixin, non-enumerable properties should also be copied, because the accessor methods of the enumerable properties may be reliant on some non-enumerable properties. The second type of mixin uses the assignment operator to assign the values of an object’s properties to another. This type can be used to “initialize” an object. In this type of mixin, perhaps we should not include the non-enumerable properties, since they will be created as enumerable properties on the destination object.
    Of course, the ECMAScript 5 compatibility is another issue, which I did not mean to explore here.

Understanding JavaScript Promises E-book Cover

Demystify JavaScript promises with the e-book that explains not just concepts, but also real-world uses of promises.

Download the Free E-book!

The community edition of Understanding JavaScript Promises is a free download that arrives in minutes.