I thought I know the Function definition, execution context and the behavior of this in JavaScript. However, I realized that actually I don’t or the knowlege is still not firmly grounded in my mind when I wrote some code similar to below snippet but have no instinct of the error.
var TestObj = {
a: function() {
console.log('A');
},
b: function() {
console.log('B');
this.a();
}
};
TestObj.b();
var c = TestObj.b;
c();
The result will be as below, right?
B
A
B
A
You might suspiciously answer No but If your instint doesnot tell you that and why, then you don’t know JavasScript well either like me. The result actually is:
B
A
B
TypeError: Object [object global] has no method 'a'
It is a little bit awkward or counterintuitive at first glance but it’s JavaScript. It’s the feature and amazing part. Let’s break it down piece by piece and see why.
Function definition
The TestObj includes two methods. The Function definition there actually creates two anonymous functions and then the references to the functions are assigned to the properties a and b. Those two functions are not owned by TestObj and just referred by the two properties of TestObj. This is the most important part causes the confusion. Hence, above code has not much difference than below except now we assign a name B for one of the function:
function B() {
console.log('B');
this.a();
};
var TestObj = {
a: function() {
console.log('A');
},
b: B
};
this
In ECMA-262 edition 5.1:
10.4.3 Entering Function Code
The following steps are performed when control enters the execution context for function code contained in
function object F, a caller provided thisArg, and a caller provided argumentsList:
- If the function code is strict code, set the ThisBinding to thisArg.
- Else if thisArg is null or undefined, set the ThisBinding to the global object.
- Else if Type(thisArg) is not Object, set the ThisBinding to ToObject(thisArg).
- Else set the ThisBinding to thisArg.
…
this is a special keyword refers to the binding object in the current execution context of the Function.
Once we invoke the Function through Object method, the this inside the Function body actually has been set to the TestObj instance. Hence, TestObj.b() logs B and A consecutively because this.a exists as a property of TestObj.
However, below statements mean differently.
var c = TestObj.b;
c();
Actually, variable c is just another reference pointing to Function B. Hence c() is same as B(). When directly invoking Function B, the this is bound to global object. Because there is no a defined in the global object, error occurs.
How to set a particular object as this to function
It’s commonly known that call and apply method can be called on the Function object providing a specific object as this, say:
var c = TestObj.b;
c.call(TestObj);
The result is desirable. However, this approach invokes the Function immediately. This is normally not the case that a Function has to be assigned to a Reference and passed around which is meant to be executed dynamically, like:
function dynamic(fn) {
fn();
}
dynamic(TestObj.b);
In this case, we should not use fn.call(TestObj) or fn.apply(TestObj) because it’s a generic Function which should have no knowledge on the Function passed in. Hence, above is not working.
There is still another lifesaver though. The bind method of Function. This method can take the passed in Object like what call or apply does, but it returns a new Function whose this binding is set to the Object passed in. So, above code can be revised as:
function dynamic(fn) {
fn();
}
dynamic(TestObj.b.bind(TestObj));
It’s fun, isn’t it?
[Edited on 2013/06/17]: Today, I saw another case which maybe confusing too.
var length = 3;
function logLength() {
console.log(this.length);
}
var TestObj = {
length: 2,
b: logLength,
c: function() {
(function(fn) {
arguments[0]();
})(logLength);
}
};
TestObj.b();
TestObj.c();
What do you think the console should log? Will it be 2 and 3? Actually, the result is 2 and 1. Because the TestObj.c() actually is calling the function logLength on the arguments Object, and then the this.length is referring to its own length, which is 1.
More fun, right?