JavaScript object model basics
The JavaScript object model is very often misunderstood subject among programmers. I'd like to give my own attempt at describing some important bits. You may also want to check the talk JavaScript basics that yashke gave at September DRUG drink-up.
Objects and prototypes
The idea is really simple. Every object is a dictionary that maps from string keys to values. Values can be any JavaScript values, like numbers, objects and functions. Moreover, every object has a special pointer to another object – its prototype, available via __proto__ property. When you look up object's property and it does not exist, the lookup continues to the object's __proto__, then to __proto__'s __proto__ and so on. At the end of this chain stays an object known as Object.prototype. It's __proto__ is null and the lookup terminates.
Here's an example:
> var x = {}
> x.__proto__
{}
> x.__proto__ === Object.prototype
true
> x.__proto__.__proto__
null
> x.__proto__.lorem = "ipsum"
> x.lorem
'ipsum'
I bet no-one would complain about JavaScript complexity if things were just like above. Unfortunately, the language complicates and obfuscates this simple idea.
First of all, __proto__ is non-standard property. Of course every JavaScript implementation has to store __proto__ somewhere, but only some of them expose __proto__ to the programmer. The standard ways to set prototypes are either constructor functions or Object.create. Keep in my mind the latter is not available in older browsers. We will describe the first approach.
Let's say we have a function Foo. Every function in JavaScript is an object. Every function object has a property called prototype. When you evaluate new Foo(), JavaScript executes the following:
- create new, empty object: {}
- set its __proto__ to Foo.prototype
- execute Foo function with the newly created object available as this
- return the newly created object, unless Foo returned another object (then return this another object)
Because Foo.prototype is an object, it's __proto__ already points to another object (it might be Object.prototype or something else) and this is how the __proto__ chain forms.
There are some corner cases, like what happens if Foo.prototype is not an object, but we won't dig into such details. Check the spec's section 13.2.2, if you're really interested.
What about this?
this is a special variable that holds a "current" object. It's most sane to think about it as another argument to every function, often passed implicitly by the language.
There are 4 ways of calling a function in JavaScript:
- foo()
- obj.foo() / obj["foo"]()
- foo.call() / foo.apply()
- new foo()
The critical difference between them is what becomes this during the function's execution.
- In first case, the host object (window in browsers) becomes this. This is often confusing.
- In second case, obj becomes this. This makes sense.
- call() and apply() are ways to set this explicitly to any object you want. The difference between call and apply is how you pass other arguments – either as an array or not. foo.call(obj, 1, 2) is equivalent to foo.apply(obj, [1, 2]).
- The last case, using new, works as described previously.
Keep this under your control
It's very important to understand the difference between first and second form. In JavaScript, the invocation operator () distinguishes whether it's applied to a refinement (an expression with dot or []). (11.2.3 in the spec).
> var obj = { foo: function () { return this; } }
> obj.foo()
{ foo: [Function] } /* our obj */
> var f = obj.foo; f()
{ ... } /* host object, i.e. window in browser */
This distinction often leads to mistakes, especially when passing callback functions. Imagine the following code:
var obj = {
elem: document.getElementById("results"),
loadData: function (callback) {
callback();
},
showResults: function () {
this.elem.innerHTML = "results";
}
};
obj.loadData(obj.showResults);
Can you see the mistake? Of course loadData will execute callback, losing the this value, and showResults will refer to elem property on host object, not obj! Hunting bugs like this is not funny.
There are a couple of techniques for dealing with this problem. One of them is using Function.prototype.bind(), which was introduced in ECMAScript 5 (again, not available in some older browsers). Hopefully by now it's clear that Function.prototype.bind is a property bind available on all function objects (because they have __proto__ set to Function.prototype).
Back to our example:
obj.showResults = obj.showResults.bind(obj);
obj.loadData(obj.showResults);
bind returns new function, a wrapper that, once executed, will showResults.call(obj), thus making sure that this is the object we wanted.
If you're using CoffeeScript, it's a good idea to always define methods using fat arrow =>, which is compiled to binding methods during object creation. (To be more specific, CoffeScript has more than one way to compile fat arrow).
Summary
There are some crazy quirks in JavaScript, but prototypes are not among them.