In JavaScript, the special variable this is relatively complicated, because it is available everywhere, not just in object-oriented settings. This blog post explains how this works and where it can cause problems, concluding with best practices.
To understand this, it is best to partition the locations where it can be used into three categories:
function sloppyFunc() { console.log(this === window); // true } sloppyFunc();
function strictFunc() { 'use strict'; console.log(this === undefined); // true } strictFunc();
function func(arg1, arg2) { console.log(this); // a console.log(arg1); // b console.log(arg2); // c } func.call('a', 'b', 'c'); // (this, arg1, arg2) func.apply('a', ['b', 'c']); // (this, arrayWithArgs)
var savedThis; function Constr() { savedThis = this; } var inst = new Constr(); console.log(savedThis === inst); // trueImplemented in JavaScript, the new operator looks roughly as follows (a more accurate implementation is slightly more complex):
function newOperator(Constr, arrayWithArgs) { var thisValue = Object.create(Constr.prototype); Constr.apply(thisValue, arrayWithArgs); return thisValue; }
var obj = { method: function () { console.log(this === obj); // true } } obj.method();
<script> console.log(this === window); // true </script>In Node.js, you normally execute code in modules. Therefore, the top-level scope is a special module scope:
// `global` (not `window`) refers to global object: console.log(Math === global.Math); // true // `this` doesn’t refer to the global object: console.log(this !== global); // true // `this` refers to a module’s exports: console.log(this === module.exports); // true
If eval() is called indirectly, this refers to the global object:
> (0,eval)('this === window') trueOtherwise, if eval() is called directly, this remains the same as in the surroundings of eval(). For example:
// Real functions function sloppyFunc() { console.log(eval('this') === window); // true } sloppyFunc(); function strictFunc() { 'use strict'; console.log(eval('this') === undefined); // true } strictFunc(); // Constructors var savedThis; function Constr() { savedThis = eval('this'); } var inst = new Constr(); console.log(savedThis === inst); // true // Methods var obj = { method: function () { console.log(eval('this') === obj); // true } } obj.method();
function Point(x, y) { this.x = x; this.y = y; } var p = Point(7, 5); // we forgot new! console.log(p === undefined); // true // Global variables have been created: console.log(x); // 7 console.log(y); // 5Thankfully, you get a warning in strict mode (this === undefined):
function Point(x, y) { 'use strict'; this.x = x; this.y = y; } var p = Point(7, 5); // TypeError: Cannot set property 'x' of undefined
/** Similar to setTimeout() and setImmediate() */ function callIt(func) { func(); }If you call a sloppy-mode method as a function, this refers to the global object and global variables will be created:
var counter = { count: 0, // Sloppy-mode method inc: function () { this.count++; } } callIt(counter.inc); // Didn’t work: console.log(counter.count); // 0 // Instead, a global variable has been created // (NaN is result of applying ++ to undefined): console.log(count); // NaNIf you call a strict-mode method as a function, this is undefined. Things don’t work, either. But at least you get a warning:
var counter = { count: 0, // Strict-mode method inc: function () { 'use strict'; this.count++; } } callIt(counter.inc); // TypeError: Cannot read property 'count' of undefined console.log(counter.count);The fix is to use bind():
var counter = { count: 0, inc: function () { this.count++; } } callIt(counter.inc.bind(counter)); // It worked! console.log(counter.count); // 1bind() created a new function that always receives a this whose value is counter.
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
this.friends.forEach(
function (friend) {
console.log(this.name+' knows '+friend);
}
);
}
};
obj.loop();
// TypeError: Cannot read property 'name' of undefined
In the previous example, this.name fails, because the function’s this is undefined, it is not the same as the this of the method loop(). There are three ways to fix … this.
Fix 1: that = this. Assign this to a variable that isn’t shadowed (another popular name is self) and use that one.
loop: function () { 'use strict'; var that = this; this.friends.forEach(function (friend) { console.log(that.name+' knows '+friend); }); }
Fix 2: bind(). Use bind() to create a function whose this always has the correct value (the method’s this in the following example).
loop: function () {
'use strict';
this.friends.forEach(function (friend) {
console.log(this.name+' knows '+friend);
}.bind(this));
}
Fix 3: forEach’s second parameter. This method has a second parameter whose value is passed to the callback as this.
loop: function () {
'use strict';
this.friends.forEach(function (friend) {
console.log(this.name+' knows '+friend);
}, this);
}
loop: function () { 'use strict'; // The parameter of forEach() is an arrow function this.friends.forEach(friend => { // `this` is loop’s `this` console.log(this.name+' knows '+friend); }); }I don’t like APIs that use this as an additional parameter of real functions:
beforeEach(function () {
this.addMatchers({
toBeInRange: function (start, end) {
...
}
});
});
Turning such an implicit parameter into an explicit one makes things more obvious and is compatible with arrow functions.
beforeEach(api => { api.addMatchers({ toBeInRange(start, end) { ... } }); });