this
in JavaScriptIn this blog post, I take a different approach to explaining this
in JavaScript: I pretend that arrow functions are the real functions and ordinary functions a special construct for methods. I think it makes this
easier to understand – give it a try.
In this post, we focus on two different kinds of functions:
function () {}
() => {}
An ordinary function is created as follows.
function add(x, y) {
return x + y;
}
Each ordinary function has the implicit parameter this
that is always filled in when it is called. In other words, the following two expressions are equivalent (in strict mode).
add(3, 5)
add.call(undefined, 3, 5);
If you nest ordinary functions, this
is shadowed:
function outer() {
function inner() {
console.log(this); // undefined
}
console.log(this); // 'outer'
inner();
}
outer.call('outer');
Inside inner()
, this
does not refer to the this
of outer()
, because inner()
has its own this
.
If this
were an explicit parameter, the code would look as follows:
function outer(_this) {
function inner(_this) {
console.log(_this); // undefined
}
console.log(_this); // 'outer'
inner(undefined);
}
outer('outer');
Note that inner()
shadowing the this
of outer()
is also how variables work in nested scopes:
const _this = 'outer';
console.log(_this); // 'outer'
{
const _this = undefined;
console.log(_this); // undefined
}
Due to ordinary functions always having the implicit parameter this
, a better name for them would have been “methods”.
An arrow function is created as follows (I’m using a block body so that it looks similar to a function definition):
const add = (x, y) => {
return x + y;
};
If you nest an arrow function inside an ordinary function, this
is not shadowed:
function outer() {
const inner = () => {
console.log(this); // 'outer'
};
console.log(this); // 'outer'
inner();
}
outer.call('outer');
Due to how arrow functions behave, I also occasionally call them “real functions”. They are similar to functions in most programming languages – much more so than ordinary functions.
Note that the this
of an arrow function can not be influenced via .call()
. Or in any other way – it is always determined by the scope surrounding the arrow function when it was created. For example:
function ordinary() {
const arrow = () => this;
console.log(arrow.call('goodbye')); // 'hello'
}
ordinary.call('hello');
An ordinary function becomes a method if it is the value of an object property:
const obj = {
prop: function () {}
};
One way of accessing the properties of an object is via the dot operator (.
). This operator has two different modes:
obj.prop
obj.prop(x, y)
The latter is equivalent to:
obj.prop.call(obj, x, y)
You can see that, once again, this
is always filled in when ordinary functions are invoked.
JavaScript has special, convenient, syntax for defining methods:
const obj = {
prop() {}
};
Let’s take a look at common pitfalls, through the lens of what we have just learned.
this
in callbacks (Promises) Consider the following Promise-based code where we log 'Done'
, once the asynchronous function cleanupAsync()
is finished.
// Inside a class or an object literal:
performCleanup() {
cleanupAsync()
.then(function () {
this.logStatus('Done'); // (A)
});
}
The problem is that this.logStatus()
in line A fails, because this
doesn’t refer to the this
of .performCleanup()
– it is shadowed by the this
of the callback. In other words: we have used an ordinary function when we should have used an arrow function. If we do so, everything works well:
// Inside a class or an object literal:
performCleanup() {
cleanupAsync()
.then(() => {
this.logStatus('Done');
});
}
this
in callbacks (.map()
) Similarly, the following code fails in line A, because the callback shadows the this
of method .prefixNames()
.
// Inside a class or an object literal:
prefixNames(names) {
return names.map(function (name) {
return this.company + ': ' + name; // (A)
});
}
Again, we can fix it by using an arrow function:
// Inside a class or an object literal:
prefixNames(names) {
return names.map(
name => this.company + ': ' + name);
}
The following class is for a UI component.
class UiComponent {
constructor(name) {
this.name = name;
const button = document.getElementById('myButton');
button.addEventListener('click', this.handleClick); // (A)
}
handleClick() {
console.log('Clicked '+this.name); // (B)
}
}
In line (A), UiComponent
registers an event handler for clicks. Alas, if that handler is ever triggered, you’ll get an error:
TypeError: Cannot read property 'name' of undefined
Why? In line A, we have used the normal dot operator, not the special method call dot operator. Therefore, the function stored in handleClick
becomes the handler. That is, roughly the following things happen.
const handler = this.handleClick;
handler();
// same as: handler.call(undefined);
As a consequence, this.name
fails in line B.
So how can we fix this
? The problem is that the dot operator for calling a method is not simply a combination of first reading the property and then calling the result. It does more. Therefore, when we extract a method, we need to provide the missing piece ourselves and fill in a fixed value for this
, via the function method .bind()
(line A):
class UiComponent {
constructor(name) {
this.name = name;
const button = document.getElementById('myButton');
button.addEventListener(
'click', this.handleClick.bind(this)); // (A)
}
handleClick() {
console.log('Clicked '+this.name);
}
}
Now, this
is fixed and won’t be changed via normal function calls.
function returnThis() {
return this;
}
const bound = returnThis.bind('hello');
bound(); // 'hello'
bound.call(undefined); // 'hello'
The easiest way of avoiding problems with this
is by avoiding ordinary functions and always using either method definitions or arrow functions.
However, I do like function declarations syntactically. Hoisting is also occasionally useful. You can use those safely if you don’t refer to this
inside them. There is an ESLint rule that helps you with that.
this
as if it were a parameter Some APIs provide parameter-like information via this
. I don’t like that, because it prevents you from using arrow functions and goes against the initially mentioned easy rule of thumb.
Let’s look at an example: the function beforeEach()
passes an API object to its callback via this
.
beforeEach(function () {
this.addMatchers({ // access API object
toBeInRange: function (start, end) {
···
}
});
});
This function could easily be rewritten:
beforeEach(api => {
api.addMatchers({
toBeInRange(start, end) {
···
}
});
});
this
]