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:
- In functions: this is an extra, often implicit, parameter.
- Outside functions (in the top-level scope): this refers to the global object in browsers and to a module’s exports in Node.js.
- In a string passed to eval(): eval() either picks up the current value ofthis or sets it to the global object, depending on whether it is called directly or indirectly.
Let’s examine each of these categories.
this in functions
That’s the most common way of using
this, because functions represent all callable constructs in JavaScript, by playing three different roles:
- Real functions (this is the global object in sloppy mode, undefined in strict mode)
- Constructors (this refers to the newly created instance)
- Methods (this refers to the receiver of the method call)
In functions,
this can be thought of as an extra, often implicit, parameter.
this in real functions
In real functions, the value of
this depends on the
mode one is in:
That is,
this is an implicit parameter that is set to a default value (
window or
undefined). You can, however, make a function call via
call() or
apply() and specify the value of
this explicitly:
function func(arg1, arg2) {
console.log(this);
console.log(arg1);
console.log(arg2);
}
func.call('a', 'b', 'c');
func.apply('a', ['b', 'c']);
this in constructors
Functions become constructors if you invoke them via the
new operator. That operator creates a new object and passes it to the constructor via
this:
var savedThis;
function Constr() {
savedThis = this;
}
var inst = new Constr();
console.log(savedThis === inst);
Implemented 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;
}
this in methods
In methods, things are similar to more traditional object-oriented languages:
this refers to the
receiver, the object on which the method has been invoked.
var obj = {
method: function () {
console.log(this === obj);
}
}
obj.method();
this in the top-level scope
In browsers, the top-level scope is the global scope and
this refers to the
global object(like
window does):
<script>
console.log(this === window);
</script>
In Node.js, you normally execute code in modules. Therefore, the top-level scope is a special
module scope:
console.log(Math === global.Math);
console.log(this !== global);
console.log(this === module.exports);
this in eval()
eval() can be called either
directly (via a real function call) or
indirectly (via some other means). The details are explained
here.If
eval() is called indirectly,
this refers to the
global object:
> (0,eval)('this === window')
true
Otherwise, if
eval() is called directly,
this remains the same as in the surroundings of
eval(). For example:
function sloppyFunc() {
console.log(eval('this') === window);
}
sloppyFunc();
function strictFunc() {
'use strict';
console.log(eval('this') === undefined);
}
strictFunc();
var savedThis;
function Constr() {
savedThis = eval('this');
}
var inst = new Constr();
console.log(savedThis === inst);
var obj = {
method: function () {
console.log(eval('this') === obj);
}
}
obj.method();
this-related pitfalls
There are three
this-related pitfalls that you should be aware of. Note that in each case,
strict mode makes things safer, because
this is
undefined in real functions and you get warnings when things go wrong.
Pitfall: forgetting new
If you invoke a constructor and forget the
new operator, you are accidently using it as a real function. Hence,
this does not have the correct value. In sloppy mode,
this is
window and you’ll create global variables:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p = Point(7, 5);
console.log(p === undefined);
console.log(x);
console.log(y);
Thankfully, 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);
Pitfall: extracting methods improperly
If you retrieve the value of a method (instead of invoking it), you turn the method into a function. Calling the value results in a function call, not a method call. This kind of extraction can happen when you pass a method as an argument for a function or method call. Real-world examples include
setTimeout() and registering event handlers. I’ll use the function
callIt() to simulate this use case synchronously:
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,
inc: function () {
this.count++;
}
}
callIt(counter.inc);
console.log(counter.count);
console.log(count);
If 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,
inc: function () {
'use strict';
this.count++;
}
}
callIt(counter.inc);
console.log(counter.count);
The fix is to use
bind():
var counter = {
count: 0,
inc: function () {
this.count++;
}
}
callIt(counter.inc.bind(counter));
console.log(counter.count);
bind() created a new function that always receives a
this whose value is
counter.
Pitfall: shadowing this
When you use a real function inside a method, it is easy to forget that the former has its own
this (even though it has no need for it). Therefore, you can’t refer from the former to the method’s
this, because it is shadowed. Let’s look at an example where things go wrong:
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
this.friends.forEach(
function (friend) {
console.log(this.name+' knows '+friend);
}
);
}
};
obj.loop();
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);
}
Best practices
Conceptually, I think of real functions as not having their own
this and think of the aforementioned fixes as keeping up that illusion. ECMAScript 6 supports this approach via
arrow functions – functions without their own
this. Inside such functions, you can freely use
this, because there is no shadowing:
loop: function () {
'use strict';
this.friends.forEach(friend => {
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) {
...
}
});
});