The this keyword, and why it keeps changing
Every other name in JavaScript means what the place it was written says it
means. That is lexical scope, and two lessons have now relied on it. this is
the exception, and that exception is why it confuses people who have been
writing JavaScript for years.
The one rule
this is decided by how a function is called, not by where it was written.
The same function, called two ways, gets two different this values. That is the
whole difficulty, and once you accept it the rest is a short list of cases.
Case 1: called as a method
const order = {
plates: 3,
rate: 80,
total() {
return this.plates * this.rate;
},
};
console.log(order.total());
240
The function was called as order.total(), so this is order. Look at what
is immediately to the left of the dot — that is this. This covers most of
your everyday use and is entirely unsurprising.
Case 2: the same function, detached
Now pull it off the object and call it on its own:
const total = order.total;
console.log(total());
TypeError: Cannot read properties of undefined (reading 'plates')
Identical function. Different call. Nothing to the left of a dot, so this is
undefined, and undefined.plates throws.
This is not a contrived move. It is what happens every time you pass a method somewhere:
setTimeout(order.total, 1000);
button.addEventListener('click', order.total);
Both of those hand the function over, detached from order. When it runs, the
object is long forgotten.
One caveat worth knowing: in old-style non-strict code, this in a detached call
is the global object rather than undefined, so instead of an error you get
NaN — a wrong answer instead of a crash. Modules and classes are always strict,
so you will normally get the error, which is better.
Case 3: lost inside a callback
The commonest real form of the bug:
const shop = {
name: 'Anna Tiffins',
items: ['dal', 'rice'],
list() {
return this.items.map(function (item) {
return `${item} from ${this.name}`;
});
},
};
console.log(shop.list());
TypeError: Cannot read properties of undefined (reading 'name')
Read it carefully. this.items worked — we got into map — so this was
shop at the top of list. But the function passed to map is called by
map, not by you, and not with a dot. So inside it this is undefined.
this changed halfway down a single function. Nothing else in JavaScript
does that.
The fix: arrow functions
An arrow function has no this of its own. It uses whatever this meant in the
place it was written — which is ordinary lexical scope, finally.
const shop = {
name: 'Anna Tiffins',
items: ['dal', 'rice'],
list() {
return this.items.map((item) => `${item} from ${this.name}`);
},
};
console.log(shop.list());
[ 'dal from Anna Tiffins', 'rice from Anna Tiffins' ]
The arrow was written inside list, where this is shop, so that is what it
uses. It cannot be detached from it, because it never had its own to lose.
This is what arrow functions were added for. The shorter syntax is a bonus.
The mirror image: never an arrow as a method
Because an arrow takes this from where it was written, writing one directly on
an object is exactly wrong:
const order = {
plates: 3,
rate: 80,
total: () => this.plates * this.rate,
};
console.log(order.total());
TypeError: Cannot read properties of undefined (reading 'plates')
The arrow was written at the top level, not inside anything, so its this is
whatever the top level's is — undefined in a module. It is not order, and no
way of calling it will make it order.
So: arrow functions for callbacks, ordinary functions for methods. The rule from the first lesson of this module, now with its reason.
Forcing it: call, apply and bind
Three ways to set this explicitly.
const order = { plates: 3, rate: 80 };
function total(discount) {
return this.plates * this.rate - discount;
}
console.log(total.call(order, 40));
console.log(total.apply(order, [40]));
const orderTotal = total.bind(order);
console.log(orderTotal(40));
200
200
200
| Method | What it does |
|---|---|
fn.call(thisValue, a, b) |
Calls it now, arguments listed. |
fn.apply(thisValue, [a, b]) |
Calls it now, arguments in an array. |
fn.bind(thisValue) |
Returns a new function permanently tied to that this. Does not call it. |
bind is the useful one, and it is the pre-arrow solution to case 2:
setTimeout(order.total.bind(order), 1000);
You will see bind throughout older code and React class components. In new code
an arrow usually reads better.
The full picture
| How it is called | What this is |
|---|---|
obj.method() |
obj — whatever is left of the dot |
fn() standalone |
undefined in strict mode; the global object otherwise |
| Arrow function | Whatever this was where it was written |
fn.call(x) / fn.apply(x) / fn.bind(x) |
x |
new Fn() |
The newly created object — module 5 |
| DOM event handler | The element the listener is attached to — module 7 |
The DOM row is why event handlers deserve care: an ordinary function handler gets
the element as this, which is occasionally handy and usually not what you
meant. Module 7 uses event.currentTarget instead, which says what it means.
When to avoid the question
Honest advice to finish with: a great deal of good JavaScript uses this
hardly at all.
If a function needs some data, pass the data in. Closures, from the last lesson,
give you private state with no this anywhere. this becomes genuinely
necessary with classes, in module 5, and when working with libraries that expect
it.
So learn the rules — you need them to read other people's code and to survive interviews — and then write code that does not lean on them.
Check your work
order.total() is 240. Called with order to the left of the dot, so
this is order.
const total = order.total; total(); gives TypeError: Cannot read properties of undefined (reading 'plates'). Same function, no dot, so this is
undefined in strict mode. In non-strict code you would get NaN instead — a
wrong answer rather than an error.
Inside map(function (item) {...}), this is undefined even though
this.items worked one line earlier. map calls the function, and not as a
method. This is this changing part-way through one function.
The arrow version works, giving
[ 'dal from Anna Tiffins', 'rice from Anna Tiffins' ], because an arrow has no
this of its own and uses the one from where it was written.
An arrow used as a method fails, because it takes this from the top level
rather than from the object. No way of calling it fixes that.
call, apply and bind all give 200 for 3 × 80 − 40. call takes
arguments listed, apply takes them in an array, and bind returns a new
function rather than calling it.
call cannot rescue an arrow either. arrowMethod.total.call(order) fails
with the same error. An arrow's this is fixed at the point it was written, and
nothing can reassign it.
Re-binding a bound function does nothing. bound.bind(other) returns a
function that still uses the first binding — with o at 3 plates and other
at 10, it still gives 240, not 1000. The first bind wins permanently.
Which to use for a callback? An arrow. For an object method? An ordinary
function. To hand a method to setTimeout without an arrow? bind.
Practice
- Build the
orderobject with atotal()method and call it. Confirm240. - Detach it. Assign
order.totalto a variable, call it, and read the error. Say which part of the message tells youthiswasundefined. - Fix the detached call three ways:
call,bind, and wrapping it in an arrow. - Reproduce the callback bug. Write
shop.list()with an ordinary function insidemapand watch it fail afterthis.itemssucceeded. This is the one worth dwelling on —thischanged inside a single function. - Fix it with an arrow. Then explain, in one sentence, why the arrow does not have the problem.
- Do the mirror image. Write a method as an arrow directly on an object and
watch it fail. Confirm that
callcannot rescue it either — an arrow'sthiscannot be set. - Write a function using
this.platesand call it withcall, thenapply, then a bound copy. Confirm all three agree. - Take a bound function and try to re-bind it to a different object. Note what happens — binding is permanent.
- Harder. Rewrite the
shopobject so it works with nothisat all, using a closure instead. Compare the two versions and decide which you would rather hand to somebody else to maintain.
Next: callbacks and higher-order functions — passing functions around as values,
which is what map was doing all through this lesson.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship