My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Wednesday, March 16, 2011

Object.defineHybridProperty

Update Yes, I did it: getters and setters for IE < 9 and other browsers


After my early Hooorrayyyy! about compatible IE < 9 getters and setters, I have been experimenting a bit more on how to solve the JSObject to VBVariant and vice-versa assignment and the result was an horrendous monster loads of potential memory leaks and performances implications for the already slow bounce of browsers such IE8, 7, and 6.
Since limitations were also too many, as described in this even earlier attempt from dojo framework, I have realized that VBScript was simply a no-go, or better, probably the wrong answer to the question: how can I have cross-browser getters and setters?

The jQuery Hybrid Answer

Back in 2009, James Padolsey described the jQuery framework as a sort of getters/setters simulator API, comparing the semantic and beauty of non-standard Spidermonkey __defineGetter__ and __defineSetter__, against jQuery coding style, where many "methods" could be considered as getters or setters.

// get the innerHTML of the first node found through the selector
$("#selector").html();

// set the innerHTML of the first(?) node found through the selector
$("#selector").html("
");

As showed above, the jQuery().html method can be considered a friendly answer for a cross browser get/set implementation, and surely much more friendly than what antimatter15 proposed some time ago (Pssss! dude, I could not find your real name in the "About" section ...).

My "due jQuery success, why not!" Proposal

In ES5 everything is so natural and simple, Object.defineProperty and Object.defineProperties work like a charm and IE9 with all other browsers as well. We may decide to use good old __defineGetter/Setter__ as fallback for older Opera, Chrome, Safari, or Firefox, but not for IE since these methods are not supported at all.
However, even using these fallbacks, the descriptor object will result inconsistent because writable, enumerable, and configurable properties won't act as expected.
At this point I have decided to fallback into a "jQuery approach" solution that will behave exactly the same in all old and newer browsers, being still able to use an ES5 like descriptor that in a not that far away future won't even need to be changed at all, it will simply work.

A generic Person.prototype Descriptor



var personDescriptor = {
// requires council notification on change
name: {
get: function () {
return this._name;
},
set: function (name) {
// notify here the council about this change
this._name = name;
}
},
// this property is unfortunately immutable (via public access)
age: {
get: function () {
return this._age;
}
},
// simply a method that occurs once per year
birthday: {
value: function () {
this._age++;
}
}
};


Current ES5 Person "Class" Example



//* ES5 example
function Person(name, age) {
// a new Person in town
this._age = age || 0; // default, just born
this.name = name;
}

Object.defineProperties(Person.prototype, personDescriptor);

var me = new Person("Andrea", 32);
// will throw an error
// me.age = 20;
alert([me.name, me.age]); // Andrea, 32
me.birthday();
alert(me.age); // 33
//*/


Current ES3 Person "Class" Example



//* ES3 example
Person = function Person(name, age) {

// a new Person in town
this._age = age || 0; // default, just born

// we still want to notify the council,
// no direct this._name set
this.name(name);

}

Object.defineHybridProperties(Person.prototype, personDescriptor);

var me = new Person("Andrea", 32);
// nothing will happen
// me.age(20);
alert([me.name(), me.age()]); // Andrea, 32
me.birthday();
alert(me.age()); // 33

// wanna know what they are?
alert(me.name);
alert(me.age);
alert(me.birthday);
//*/

The source code of Object.defineHybridProperty, together with Object.defineHybridProperties, is here and as you can see it's a quite simple and compact piece of code.

Use Cases and DONTS

The function name should be explicit enough, and it's used to define hybrid properties.
Hybrid properties could be confused with methods ... and actually, all hybrid properties that have a get and/or set descriptor, become de-facto an object methods with current constrain: zero arguments to get, 1 single argument to set.
Use cases have been already described, a jQuery like API could use without problems the current approach, making the future ES5 only refactory less painful than whatever other get/set approach.
Indeed, once we know which descriptor is using getters/setters, all we have to do is to remember which property has been made hybrid, and change accordingly each invoke with arguments as assignment, and removing brackets from every other empty invoke.
Please Note I will update later the code in order to properly assign Object.prototype native names such toString so that IE browsers will consider them as well.
Done, it's updated and 409 bytes minified and gzipped, have fun :)

9 comments:

Darktalker said...

I'm very enjoying your posts :)
thanks for the update.
Now I can safely define setter/getter in my code :)

Maroo said...

Ok lets say that we can create cross browser getters and setters but I should do that. I really share opinion that in most cases it's just bad design and over used practice. To see why i put link to article which describe most of drawbacks of accessors http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html

Andrea Giammarchi said...

Maroo, 3/4 of the JavaScript you use on daily basis is based on getters and setters behind the scene ... just think about generic array.length

Of course, use with caution ;)

Chris said...

I like the approach but I worry about mistakes during the ES3 to 5 refactor. Using a naming convention for the generated hybrid property methods could simplify the refactor to two simple regexes. So in your example the generated code could look like -

// nothing will happen
// me.$age(20);
alert([me.$name(), me.$age()]); // Andrea, 32
me.birthday();
alert(me.$age()); // 33

(note the birthday method remains unchanged as it is not a property)

Advantages - very easy to refactor, because methods are easily distinguishable from properties
Disadvantages - slight increase in code size (and ugliness ;))

I suppose it comes down to the size and longevity of the code base (i.e. how many people will be maintaining it and will it still be around when ES5 is being targeted) as to whether it would be of any benefit.

Steve Clay said...

I like this. The hiddenNative code could use some inline documentation. E.g. for what case is this needed?

Andrea Giammarchi said...

you can do it already, just use "$age" as property descriptor and change it in the definition object after the regexp :)

Andrea Giammarchi said...

@Steve Clay
in IE toString and other properties inherited from native Object.prototype won't be exposed via for/in loop ( these are not enumerable )

In these cases I need to force the hasOwnProperty check for cross-browser consistency reason

Anonymous said...

Greetings,

Thanks for sharing this link - but unfortunately it seems to be down? Does anybody here at webreflection.blogspot.com have a mirror or another source?


Cheers,
Alex

Andrea Giammarchi said...

hey, anonymous robot, you know ask for a mirror because of "down" problems is a contraindication?
How could you write anything here if it was not reachable ... good night bloggers, put above comment as one of those to filter, no matter what!!!