JavaScript this pointer is actually evil... especially if you are going to export modules.
function declareModuleTest () {
return {
funA: function funA () {
console.log("A",this);
},
funB: function funB () {
console.log("B", this);
return this.funA();
}
}
}
var module = declareModuleTest();
function testThis (callback) {
callback();
}
testThis(module.funB);
What is the expected outcome, in your opinion? What do you think it will be the value of this inside funB, when called from testThis? Chances are you haven't bet on "page will crash", but actually this is the case: an exception will be thrown at this line
return this.funA();
because the value of this will either be null or window, depending on if you are using strict mode.
If we had called funB as module.funB(), there would have been no problem, and the execution would proceed as expected: let's see why!
The Internet is crammed with literature on the infamous "this" pointer. It actually is one of those bad parts highlighted by Douglas Crockford; the main reason of its evilness is that function context depends on the way you call it.
There are, in fact, 4 different ways in which you can invoke a function, producing different values for this:
As a constructor: Using new keyword, the function is invoked as a constructor, and this is assigned the reference to the newly created Object (that, by the way, will be returned by the method).
As a method: If an object instance obj has a method m, and you call it as obj.m(), then the this pointer inside m will be the reference to obj.
As a function: Here the behaviour depends on if you are using strict mode (i.e. EcmaScript 5) or not; before ES5, the this pointer passed to the function invoked would have been a reference to the global object (i.e. window): this was one of the most common bug sources in JavaScript. In ES5 this nonsense has been partially corrected: now this will point to null inside these functions - This is still far from the ideal behaviour, however: it would have been better passing a reference to the object inside which the function is called (which would have also solved our problem above in the first place).
Using apply/call: by using these methods of the Function prototype, we can set the reference stored by this inside the function body by passing it as a first parameter to the method: funB.apply(module) or funB.call(module) would do the trick. (The difference between apply and call is that the former takes the arguments for the function as an array (its second argument), while for the latter the actual parameters must be listed after the first parameter).
Let's try another test first:
function declareModuleTest () {
return {
funA: function funA () {
console.log("A",this);
},
funB: function funB () {
console.log("B", this);
return this.funA();
}
}
}
var module = declareModuleTest();
var testThis = module.funB;
testThis();
Do you think that anything changes now? Of course not!
Turns out that, if you assign a method to a variable (either by explicitily storing it in a var or by passing it as a parameter), the Function object you are storing won't retain the information about its context, so when called it will be invoked as... a function (since it is one!).
Therefore the this pointer will be inconsistent with what the way we'd expect the function to be used. Since we have no control on how a user of our libraries will use it, using references to this in public methods (methods that, directly or indirectly, can be called from outside our module) is simply UNSAFE.
The solution, however, is really at hand:
function declareModuleTest () {
var moduleTest = {
funA: function funA () {
console.log("A", this, moduleTest);
},
funB: function funB () {
console.log("B", this, moduleTest);
return moduleTest.funA();
}
};
return moduleTest;
}
var module = declareModuleTest();
var testThis = module.funB;
testThis();
Simply create the module as a private module attribute, and replace every occurrence of this with a reference to that attribute.
To further improve the design of our module, I'd suggest a little improvement made possible by strict mode:
function declareModuleTest () {
"use strict";
var moduleTest = {
funA: function funA () {
console.log("A", this, moduleTest);
},
funB: function funB () {
console.log("B", this, moduleTest);
return moduleTest.funA();
}
};
if (Object.seal instanceof Function) {
Object.seal(moduleTest);
}
return moduleTest;
}
var module = declareModuleTest();
var testThis = module.funB;
testThis();
This way, you make sure your module can't be tampered with by users (which you might care, especially if external users act as middlemen at some point).
If you target only newer browsers versions, and you either check at the very beginning for ES5 compatibility, or you are fine with causing crashes in earlier versions (definitely do not take it as an advice! :D), you can remove the test on Object.seal.
Yet another solution, perhaps the most elegant of all, is using the Function.prototype.bind method, which is, again, available in ES5. This method allows you to force the value of this inside any function expression.
var tmpObj = {}; //Any object you might need
function declareModuleTest () {
return {
funA: function funA () {
console.log("A",this);
}.bind(this),
funB: function funB () {
console.log("B", this);
return this.funA();
}
}
}
var module = declareModuleTest();
var testThis = module.funB.bind(tmpObj);
testThis();