Thursday

More than you wanted to know about JavaScript object creation

I began this journey because I needed to create a new instance of a JavaScript class with a variable number of arguments. Not knowing the number of arguments, I couldn't just write new MyClass(arg[0],arg[1].. etc.), so I had to figure out how to replicate the behaviour of new in some other way.

You may have heard that JavaScript inheritance is prototypal. If you don't know what that means, thin of it as one object inheriting from another by making a copy of the original object. Everything in JavaScript is an object, including functions. You may think that JavaScript has classes. It doesn't. It really just has functions.

JavaScript is prototypal, but it wanted to appeal to programmers that were used to "classical" inheritance, so it included the 'new' keyword that we all love so much. That means you can do things like:

function MyClass(arg1,arg2) {
this.foo = arg1;
this.bar = arg2;
this.baz = 3;
}
var mine = new MyClass('foo','bar');
// mine.foo == 'foo'
So what's going on? MyClass is called, but there is this 'this' object that isn't declared anywhere. Where did it come from? new created it for us and put it into the scope of the MyClass function. If a function called with 'new' doesn't have a return statement, it returns 'this' by default. You could try to call MyClass by itself, but you would end up creating 3 global variables because this defaults to window. So how do we redefine this? There are two methods on all functions (remember, functions are objects, just like everything else) that let you control the value of this: call and apply.
var foo = { qux: '123' };
MyClass.call(foo,'foo','bar');
// foo.foo == 'foo' && foo.qux == '123'
// if you don't know the exact number of arguments you can use apply
var args = ['foo','bar'];
// apply takes an array for the arguments
MyClass.apply(foo,args);
You might now be thinking that MyClass.call({},'foo','bar') is the same as new MyClass('foo','bar'). Well, new does a bit more than that. Remember the prototype? Calling new more or less creates a copy of the class's prototype, which is then used as this when calling the function. e.g.

MyClass.prototype.myFunc = function(a,b) { return a+b; };
var mine = new MyClass(...);
// mine.myFunc == function(a,b) { return a+b; }


One brief digression from our quest: John Resig blogged about a way to create classes that are instantiated correctly even if you forget new. An interesting read, but not quite what we need since we may need to instantiate classes not under our control.

So if you create a new object, copy all the properties of MyClass.prototype, and then call MyClass.apply(...), is that the same as new MyClass(...)? Nope. new does some things behind the scenes that you cannot replicate without calling new. e.g.,

var mine = {};
for(var i in MyClass.prototype) {
mine[i] = MyClass.prototype[i];
}
MyClass.apply(mine,'foo','bar');
// mine instanceof MyClass == false

So, what we have is an instance that is almost, but not quite identical to an instance created with new. If you know that the object will only be duck typed, then this is good enough. However, if it's possible that it might end up in an instanceof or have some other low level type check, this isn't good enough. The final trick that you need is new:

function newInstance(type,args) {
var f = function() {};
f.prototype = type.prototype;
var o = new f;
return type.apply(o,args) || o;
}

By creating a no-arguments function with the same prototype, we can safely use new to get the prototype copying, and then call the constructor using apply. instanceof type (and other checks, like prototype.constructor) will return true, and the new instance will walk and talk like a duck of MyClass.

No comments: