Saturday, January 30, 2010

[ES5] Classes As Descriptor Objects

In my latest posts I have talked about current situation for JavaScript "Classes". In Good Old And Common JS Errors I have introduced a misconception of a generic "Class" function which is usually not able to produce instanceof Class, considering Classes in JS are functions indeed.
In Better JS Classes I have explained how to write a meaningful Class factory, avoiding the _super pollution over each extended method, while in the JS _super Bullshit I have explained why the _super/parent concept does not scale with JavaScript prototypal inheritance model.

More than a dev, Mr Crockford included, agreed that in JavaScript the classical pattern does not fit/scale/produce expected results, and even worst, it could cause "disasters" during a session and slow down overall performances (and about this topic I have already said that web developers should stop to test their stuff with powerful CPU, read Macs!
Buy a bloody Atom based device as I have done and after that we can talk about the meaning of performances, right? Netbook should be able to surf without 100% of CPU usage, do you agree?)

The best part over all these years of wrong classical inheritance implementation is that with JavaScript.Next, aka ES5, the way we can define instances does not even require a function definition. Wanna know more?

Objects Extend Objects

It's that simple, we all know this is what happens in OOP Languages based over prototypal inheritance ... and since it is that simple ...

ES5 Classes As Descriptors

Or if you prefer, as definition objects. Yep! While every framework is using functions to create instances via new, in ES5 we could completely forget this pattern.

// class descriptor/definition
var MyClass = {
getValue:function () {
return this._value;
},
setValue:function (_value) {
this._value = _value;
}
};

// MyClass instance
var mc = Object.create(

// inherits from MyClass
MyClass,

// define privileged properties or methods
{
_value:{
value:"Hello ES5"
}
}
);

// Hello ES5
alert(mc.getValue());

Pretty cool, isn't it, but I am sure somebody is already arguing something like: "... and what about instanceof?"

instanceof

The instanceof operator checks if an object inherits from the implicit constructor prototype.
A common mistake is to think that instanceof is related to the function itself while it has nothing to do with it.

function A() {};
var klass = {};
A.prototype = klass;

var a = new A;

// true
alert(a instanceof A);

A.prototype = {};

// false
alert(a instanceof A);

function B(){};
B.prototype = klass;

// true
alert(a instanceof B);

Is it clear? instanceof works only with functions and only with those functions with an implicit prototype property, the default one, or specified as a generic object.

function A() {};
A.prototype = null;

var a = new A;

// throws: 'prototype' property of A is not an object
alert(a instanceof A);

To avoid above error, being JavaScript 99% dynamic, we could use a safer check, and for this example via isPrototypeOf:


function instanceOf(o, F) {
return !!F.prototype && F.prototype.isPrototypeOf(o);
};

function A() {};
var a = new A;

// true
alert(instanceOf(a, A));

Boring, slow, etc etc ... why don't we use directly our initial class description to understand if an instance is inheriting that class?

// using first example code
alert(MyClass.isPrototypeOf(mc));
// true

As alternative, we could use the global Object.getPrototypeOf method:

Object.getPrototypeOf(mc) === MyClass;

To be honest, for both easier scope resolution and semantic, I prefer the isPrototypeOf way. Furthermore, specially if the chain is deeper than 1 level, getPrototypeOf could generate false negatives while getPrototypeOf won't.

function A() {};
function B() {};
(B.prototype = new A).constructor = B;

var b = new B;

// true
alert(Object.getPrototypeOf(b) === B.prototype);

// false
alert(Object.getPrototypeOf(b) === A.prototype);

// both TRUE!
alert(B.prototype.isPrototypeOf(b));
alert(A.prototype.isPrototypeOf(b));

Accordingly, the best function to emulate an instanceOf function over objects or "classes" could be:


function inherits(o, __proto__) {
// (C) Mit Style WebReflection suggestion
return ((
typeof __proto__ === "function" ?
__proto__.prototype :
__proto__
) || {}).isPrototypeOf(o);
};

// test case
function A() {};
function B() {};
(B.prototype = new A).constructor = B;

var b = new B;

// true,true,true,true
alert([
inherits(b, A),
inherits(b, B),
inherits(b, A.prototype),
inherits(b, B.prototype)
]);


More ES5 Friendly Patterns

If we use what ES5 is bringing into JavaScript and via native execution speed, we may be interested into more "articulated" patterns to define "classes", or simply classes instances.

Object.defineProperties

First of all, as I have twitted already, please ignore that wrong suggestion about custom implementation.
The only place where all those checks could make sense is inside Object.defineProperty, and not twice in both defineProperties AND definePrperty, since if latter exists, why on earth we should try to emulate its internal checks?
If defineProperty does NOT exists, why shoud we try to emulate it without checks? I hope MDC guys will remove that nonsense from that page, I'd love to be sure developers get ES5 properly, good practices included.
Back in the topic, here there is an example:

function A(_value) {
// implicit init method
// where we define privileged proeprties/methods
Object.defineProperties(this, {
_value:{
value:_value,
// as example since it is false by default
enumerable:false
}
});

// and eventually we perform some task
};

// Class definition
Object.defineProperties(A.prototype, {
toString:{
value:function () {
return "" + this._value;
},
configurable:false
}
});

var a = new A(123);
alert(a); // 123

Does it make sense? We have more power via defineProperties and we can reuse, or share, objects across the whole library without any kind of problem.
One thing we should be ware about, is that A.prototype is always re-assignable, so if we try to define the A prototype property itself as non configurable we won't be safer anyway, it can be overwritten!

Dual Behavior: Factory Emulator


// when use strict will be enabled
// to obtain dual behavior (factory/constructor)
function A(_value) {
"use strict";
return Object.defineProperties(this || new A(_value), {
//... privileged definition
});
};


Dual Behavior Via Object.create


// Object.create way
function A(_value) {
// note that "this" may be useless, A() same of new A()
// inheritance chained in place
return Object.create(A.prototype, {
_value:{
value:_value,
enumerable:false
}
});
};

A.prototype = Object.create(A.prototype, {
toString:{
value:function () {
return "" + this._value;
},
configurable:false
}
});


Confused ???

I know I have started saying that theoretically we don't need anymore a single function to implement inheritance via ES5, but I bet those Java guys won't ever accept the fact JavaScript has no Classes and this is why I have suggested different patterns so that everybody could be happy about these ES5 features.

When Can We Start To Use These Features

Right now, in my opinion, including just one single vice versa project Object.js file, being careful about the target browser.
Unfortunately, and as usual, Internet Explorer is behind every other browser and some method cannot be perfectly emulated.
This is also why I have decided to show both defineProperties and create way, since IE defineProperties works only with DOM prototypes and global Window (or generally speaking only with natives and not with object) so that create could be used without problems, so far avoiding get/set and considering that configuration properties may not be respected.
I know this is crap, but until IE9 or the day we'll finally decide to drop this browser support, there's not that much we can do: annoying!

P.S. to test all these natively, we can use a WebKit nightly build.

No comments:

Post a Comment