Wednesday, December 16, 2009

Getters and setters in Javascript 1.7

This is how you define getters and setters in Javascript 1.7 (Firefox 3.6):

let Foo = function () {
this._x = "hello";
};

Foo.prototype.__defineGetter__("x", function () {
return this._x;
});

Foo.prototype.__defineSetter__("x", function (x) {
this._x = x;
});


Holy crap! That's a mouthful! There is, as MDC says, another way to define getters and setters on object literals:

let foo = {
_x: "hello",
get x() {
return this._x;
},
set x(x) {
this._x = x;
}
};


It's much more concise than the previous way to define getters and setters, but there's one problem: it only works with object literals. This totally messes you up if you want to exploit Javascript's prototypical object system. For example, if you do this:

let Foo = function () {
this._x = "hello";
};

Foo.prototype = {
get x() {
return this._x;
},
set x(x) {
this._x = x;
}
};


...then you cannot defer property lookups to objects higher on the prototype chain of Foo instances in the natural way. Normally, you would do this if you wanted an instance of Bar in the prototype chain:

Foo.prototype = new Bar();


Bar could in turn contain, say, Baz, in its prototype chain (all the way up until null, as all prototype chains are terminated by null). However, there is no standard way to reference an object literal's prototype. Despite the fact that the ECMAScript spec does guarantee that every object contains a [[prototype]] property, it is hidden. You could try looking up the constructor of an object literal and changing it's prototype, but even if that worked, it would cause all object literals to have the same prototype (see below):

let foo = { a: 0, b: 1, c: 2 };
let bar = { x: 0, y: 1, z: 2 };
foo.constructor.prototype = bar;

foo.constructor.prototype === bar; // Evaluates to false
foo.x === undefined;               // Evaluates to true

// Even if foo.constructor.prototype === bar, this would be the case:
let bar = {};
bar.x === 0;                       // Would evaluate to true


However, if you don't mind that you are writing Javascript for Firefox anyways, you can set the hidden prototype property by setting the __proto__ property:

let Foo = function () {
this._x = "hello";
};

let Bar = function () {
this.y = "goodbye";
};

Foo.prototype = {
get x() { return this._x; },
set x(x) { this._x = x; }
};

Foo.prototype.__proto__ = new Bar();

let foo = new Foo();
foo.x === "hello";   // Evaluates to true
foo.y === "goodbye"; // Evaluates to true


Intuitively odd, and perhaps dangerous? Yes, but also much less visually abhorrent than __defineGetter__ or __defineSetter. For the meek, this will work as well:

let Foo = function () {
this._x = "hello";
};

let p = Foo.prototype;
p.get = p.__defineGetter__;
p.set = p.__defineSetter__;

p.get("x", function () {
return this._x;
});

p.set("x", function (x) {
this._x = x;
});