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

Thursday, July 26, 2007

625 bytes to extend JavaScript

These days I've studied (again) JavaScript prototypal inheritance and common extend functions or methods used by different libraries.
This post is a summary about my experiments and every example code is based on this script.

Probably someone is thinking about my unreadable source code, however it's self packed and quite clear, atleast for me.
The goal was to write 2 basic prototypes to extend objects or functions using "best practices" to do them using less chars as possible ... so, goal done ;-)

If You're interested about these prototypes logic please tell me, I'll try to find more time to explain better each one.

Object.prototype.extend
This is the first proposal and it's really simple to use/understand.

var a = {a:"a"},
b = {b:"b"}.extend(a);
alert([b.a, b.b]); // a,b

This is a basic example.
Extend prototype works with object itself and return them after a for in loop discarding prototyped methods.

function A(value){
this.value = value;
};
A.prototype.getValue = function(){
return this.value;
};

var a = new A("a"),
b = {}.extend(a);
alert([b.value, b.getValue]); // a,undefined

Since extend works with object itself You can easyly add properties or methods in this way:

var a = {a:"a"};
a.extend({
getValue:function(){
return this.value;
},
setValue:function(value){
this.value = value
return this.getValue();
}
});

alert([a.a, a.setValue("b")]); // a,b

This prototype accepts one or more argument, so You can produce last example in this way too:

var a = {a:"a"},
b = {setValue:function(value){
this.value = value
return this.getValue();
}};
a.extend({getValue:function(){return this.value}},b);

alert([a.a, a.setValue("b")]); // a,b

Obviously, extend prototype sets correctly toString and valueOf methods with IE too, what You just need to remember is that if extended objects have two methods with the same name, last one only will be available:

var a = {a:"a"}.extend(
{
toString:function(){return "a"},
valueOf:function(){return this.a}
},
{
toString:function(){return "Object a"}
}
);

alert([a,a.valueOf()]); // Object a,a

That's all, Object.prototype.extend is just what You need when You need to extend some object.
Only a last note about extend, it should work with other variables type too but please remember that for in loop doesn't always respect assignment order:

var a = [,,,4,5,6].extend([1,2,3]);
alert(a); // 1,2,3,4,5,6

This is "a case" and not a rule so please use concat native method to extend Arrays or test for in loops before You use them with variables that are not instances or native objects.


Function.prototype.extend
This is my second prototype proposal, partially based on first one but really more powerful.
This prototype extends constructors and return them adding special features that other libraries don't use.

Its behaviour is described on this post about JavaScript prototypal inheritance but this prototype is better than simple $extend function showed on this post.

As first point, this prototype assigns correctly constructor used to create other one.
This seems to be a "natural" behaviour but I'm sure that not every other library assign them correctly.
The constructor property is not "secure" as instanceof check but first one could be easyly compared, for example, inside a swtich:

switch(genericInstance.constrcutor){
case Array://doStuff
break;
case Mine://doOtherStuff
break;
};

With more complex code this feature is not so unusual while instanceof requires a lot of if ... else if ... else.
This property is useful for a lot of other pieces of code too, so why We shouldn't have a correct constructor property when We create or extend another one?

This is another example that has a "not everytime" respected behaviour (using other libraries):

function A(){};
function B(){};
B.extend(A);

alert([

(new B).constructor === B, // true
(new B).parent === A, // true
(new B) instanceof A, // true
(new B) instanceof B, // true
(new A) instanceof B // false

].join("\n"));


The second setted property, showed in last example too, is parent one, that's a referer to parent constructor if exists, undefined value otherwise.

This property should be useful as constructor to do one or more operations using parent insteadof constrcutor.

Third point about Function.prototype.extend is Super, automatically inherited extra special method.


What's Super?
Super is a method that supports multiple (cascade) inheritance, starting from instance parent and correctly respected on its parent too.

The parent property isn't always enought to support multiple parents methods.
Look at this example:

function A(){};
A.prototype.init = function(name){
this.name = name;
return this;
};

(B = function(){}).extend(
A,
{init:function(name){
var result = this.parent.prototype.init.call(this, name);
this.name += " from B";
return result;
}}
);

C = function(){};
C.extend(
B,
{init:function(name){
this.parent.prototype.init.call(this, name);
this.name += " from C";
return this;
}
});

If You think that a new C instance will not crash or block your browser You're wrong!
Since apply or call methods inject temporary into another method (function) scope a different this referer, You should think about B.prototype.init one.
This method will use a C instance as this referer but C instance will have its own properties and its own methods.
Infact C instance will have a parent property too that will be exactely B constrcutor so above example, using a new C instance, will loop recursively calling as many times as it can B.prototype.init.

The solution is really simple, just use explicitally constrcutor as showed in this example:

function A(){};
A.prototype.init = function(name){
this.name = name;
return this;
};

(B = function(){}).extend(
A,
{init:function(name){
var result = A.prototype.init.call(this, name);
this.name += " from B";
return result;
}}
);

C = function(){};
C.extend(
B,
{init:function(name){
B.prototype.init.call(this, name);
this.name += " from C";
return this;
}
});

alert((new C).init("A").name); // A from B from C

Is it ok? Of course, it works perfectly.
However other languages have a dedicated keyword to use constructor recoursively or not and this keyword should be parent or super one.
In JavaScript the super keyword is reserved so it should be a good idea to don't use them if it's not absolutely necessary (used as string, for example), that's why I've created a Super magic method and this is how does it work:

function A(){};
A.prototype.init = function(name){
this.name = name;
return this;
};

(B = function(){}).extend(
A,
{init:function(name){
var result = this.Super("init", name);
this.name += " from B";
return result;
}}
);

C = function(){};
C.extend(
B,
{init:function(name){
this.Super("init", name);
this.name += " from C";
return this;
}
});

alert((new C).init("A").name); // A from B from C

Simple? I hope them and this way to use inheritance is quite cool (imho) ;-)
You don't need to care about constrcutor name and You don't need to use call or apply, just specify parent method name to call or, if You need to call super constructor, just use a nullable value such 0, "", undefined, false or null.

This is, for example, the Kevin Lindsey demostration code, adapted with my extend prototype:

function Person(first, last) {
this.first = first;
this.last = last;
};
Person.prototype.toString = function() {
return this.first + " " + this.last;
};

function Employee(first, last, id){
this.Super(null, first, last);
this.id = id;
};
Employee.extend(
Person,{
toString:function() {
return this.Super("toString") + ": " + this.id;
}
});

function Manager(first, last, id, department) {
this.Super(null, first, last, id);
this.department = department;
};
Manager.extend(
Employee,{
toString:function() {
return this.Super("toString") + ": " + this.department;
}
});

alert([

new Person("John", "Dough"),
new Employee("Bill", "Joi", 10),
new Manager("Bob", "Bark", 20, "Accounting")

].join("\n"));

... that in a more scriptish way should become this piece of code:

(Manager = function(first, last, id, department){
this.Super(null, first, last, id);
this.department = department;
}).extend(
(Employee = function(first, last, id){
this.Super(null, first, last);
this.id = id;
}).extend(
(Person = function(first, last){
this.first = first;
this.last = last;
}).extend(
null,
{toString:function(){return this.first + " " + this.last}}
),
{toString:function(){return this.Super("toString") + ": " + this.id}}
),
{toString:function(){return this.Super("toString") + ": " + this.department}}
);

Last point is that if You don't specify a constructor (function) as first Function.prototype.extend argument You'll recieve just original one:

var valueOf = {valueOf:function(){return this.constructor}};
A = (function(name){
this.name = name
}).extend(
null,
{toString:function(){return this.name}},
valueOf
);

alert([(new A("test")), (new A).valueOf() === A]); // test, true

So do You like these 625 prototypal bytes?

5 comments:

Unknown said...

Fa bu lo us :D

Andrea Giammarchi said...

thank You ... however, now it's exactly 658 bytes (and now extend works with every iterable object, not only with object instances).

I suppose (and hope) this version could be marked as stable and really debugged.

Már said...

Hi. What sort of license were you considering for this library?

Andrea Giammarchi said...

I suppose Mit Style Licence :-)

In few words, just use them but let my name visible inside your library / project licence. Should be it ok?

(I didn't write licence to preserve solution size)

Már said...

ok. I'm not sure I'll use it, but I'll make sure to leave your name in if I do.

thanks.