rinat.io


Mixing in and Extending JavaScript Getters and Setters

I like to use JavaScript getters and setters. Even though they do not work in IE6-8, we can use them with node.js on server side. There's one issue though — mixing in those methods.

A lot of JavaScript libraries and frameworks have mixin helpers, for example jQuery.extend(), angular.extend() and _.extend(). It is simple and powerful, although if you try to use those methods for extending getters, here's what you'll get:

var sumMixin = {
    a: 1,
    b: 1,
    get sum() {
        return this.a + this.b;
    }
}

var Foo = function() {};
_.extend(Foo.prototype, sumMixin);

var foo = new Foo();
foo.a = 3;
foo.b = 3;
console.log(foo.sum); // will output 2

That's because after _.extend execution Foo.prototype.sum is not a method, it is actually a result of sumMixin.sum execution.

We can workaround this issue by assigning prototype directly, although if you want to add other methods to Foo.prototype you'll have to make it this way:

var Foo = function() {};
Foo.prototype = sumMixin;
Foo.prototype.bar = function() {};
Foo.prototype._test = 'test';
// And here's a getter :/
Object.defineProperty(Foo.prototype, 'test', {
    get: function() {
        return this._test;
    }
});

var foo = new Foo();
foo.a = 3;
foo.b = 3;
console.log(foo.sum); // will output 6 now

It works, but

  • we can apply only one mixin this way
  • we lose the sugar we had with extend() helpers.

If you still like magic methods and mixins you can tweak your favorite extend() method to respect getters and setters like this (based on Underscore):

_.extend = function(obj) {
    Array.prototype.slice.call(arguments, 1).forEach(function(source) {
        var descriptor, prop;
        if (source) {
            for (prop in source) {
                descriptor = Object.getOwnPropertyDescriptor(source, prop);
                Object.defineProperty(obj, prop, descriptor);
            }
        }
    });
    return obj;
};

Object.getOwnPropertyDescriptor and Object.defineProperty help us copy sum method description instead of its value to the prototype. Now mixing in works as expected:

var Foo = function() {};
_.extend(Foo.prototype, sumMixin, {
    bar: function() {},
    _test: 'test',
    get test() {
        return this._test;
    }
});

var foo = new Foo();
foo.a = 3;
foo.b = 3;
console.log(foo.sum); // will output 6