The Word this
This is the lesson that decides whether OOP ever clicks for you. this is not the object a function lives in. It is whichever object was holding the function at the moment it was called.
By the end: Predict what this is in any call, and fix it when it is wrong.
Almost everyone learns this wrong, in the same way. They learn it as "the object the function belongs to". Then one day a method stops working for no visible reason, and there is nothing in that definition to explain it.
Here is the accurate version.
A bus does not know who is driving it. Whoever sits behind the wheel is "the driver" for as long as they are sitting there. The seat is the function. The person in the seat is this. Leave the seat empty and it belongs to nobody.
function whoAmI() {
return this.plate;
}
const lagosBus = { plate: 'LAG-441-XA', whoAmI };
const enuguBus = { plate: 'ENU-318-PH', whoAmI };
console.log(lagosBus.whoAmI());
console.log(enuguBus.whoAmI());
// One function. Two results. The function never changed.
console.log(lagosBus.whoAmI === enuguBus.whoAmI);
Output
Read the call sites, not the function body. lagosBus.whoAmI() has lagosBus to the left of the dot, so this is lagosBus. That is all there is to it.
The five ways this gets set
Every value this ever takes comes from one of these.
const board = {
label: 'Lagos - Abuja',
read() { return this.label; },
};
console.log(board.read());
Output
'use strict';
const board = {
label: 'Lagos - Abuja',
read() { return this; },
};
// Take the function off the object and call it with no dot.
const loose = board.read;
console.log(loose());
Output
In a module or a class body, or any code running in strict mode, an unowned call gives this === undefined. In old sloppy-mode scripts it silently gave you the global object instead, which is how a typo could quietly write to window. Strict mode turning it into undefined is the language doing you a favour.
function Bus(plate) {
this.plate = plate;
console.log('inside the constructor, this is', this);
}
const b = new Bus('ABJ-772-KM');
console.log('and it came back as', b);
Output
function describe(prefix) {
return prefix + ' ' + this.plate;
}
const lagosBus = { plate: 'LAG-441-XA' };
// call: arguments listed one by one
console.log(describe.call(lagosBus, 'This is'));
// apply: arguments in an array
console.log(describe.apply(lagosBus, ['Boarding now:']));
// bind: no call yet, just a new function with this permanently fixed
const describeLagosBus = describe.bind(lagosBus);
console.log(describeLagosBus('Fixed to'));
Output
const park = {
name: 'Jibowu park',
regular() {
return this.name;
},
arrow: () => {
// No own `this`, so it uses whatever `this` was where it was written,
// which out here is not the park at all.
return this === undefined ? 'this is undefined' : String(this);
},
};
console.log(park.regular());
console.log(park.arrow());
Output
The breakage everyone hits
Now the failure that makes people distrust this. You pass a method somewhere as a callback, and it loses its owner in transit.
const counter = {
label: 'bookings',
count: 0,
tick() {
this.count++;
return this.label + ': ' + this.count;
},
};
console.log(counter.tick());
// Hand the method to something else. There is no dot at the eventual call site.
const runIt = counter.tick;
try {
console.log(runIt());
} catch (err) {
console.log('Broke:', err.message);
}
// Same failure, dressed up as a callback:
try {
[1, 2, 3].forEach(counter.tick);
} catch (err) {
console.log('Broke again:', err.message);
}
Output
Nothing is wrong with the method. The call site simply has no object in front of it. Three fixes, in order of how often you should reach for them.
const counter = {
label: 'bookings',
count: 0,
tick() { this.count++; return this.count; },
};
[1, 2, 3].forEach(() => counter.tick());
console.log('count:', counter.count);
Output
const counter = {
label: 'bookings',
count: 0,
tick() { this.count++; return this.count; },
};
const bound = counter.tick.bind(counter);
[1, 2, 3].forEach(bound);
console.log('count:', counter.count);
Output
class Counter {
count = 0;
// An arrow assigned as a field is created inside the constructor, where
// `this` is already the new instance. It can never be detached.
tick = () => {
this.count++;
return this.count;
};
}
const c = new Counter();
const detached = c.tick;
detached();
detached();
console.log('count:', c.count);
Output
Where arrows are exactly right
The property that breaks arrows in object literals is the same one that makes them perfect inside methods. An arrow written inside a method inherits that method's this, so callbacks just work.
const park = {
name: 'Jibowu park',
buses: ['LAG-441-XA', 'ABJ-772-KM', 'ENU-318-PH'],
labelledGood() {
// Arrow inherits `this` from labelledGood, which is the park.
return this.buses.map(b => b + ' @ ' + this.name);
},
labelledBad() {
// A regular function callback gets no owner, so this.name explodes.
return this.buses.map(function (b) {
return b + ' @ ' + this.name;
});
},
};
console.log(park.labelledGood());
try {
console.log(park.labelledBad());
} catch (err) {
console.log('labelledBad broke:', err.message);
}
Output
A checklist you can actually use
When you are unsure what this is, look at the call, and ask in this order:
- Is the function an arrow? Then ignore the call entirely and look at where the arrow was written.
- Was it called with
new? Thenthisis the new object. - Was it called with
.call,.apply, or is it the result of.bind? Thenthisis whatever was supplied. - Is there something before the dot at the call site? Then that is
this. - None of the above?
thisisundefined.
Your turn: predict, then check
Four calls below. Write down what you think each logs before you run it. Getting one wrong here is worth more than getting all four right by accident.
Output
Recap
thisis set by the call, not by where the function was written.- Look immediately to the left of the dot at the call site. That is
this. - No dot means no owner, and in strict mode that means
thisisundefined. newsetsthisto the new object;call,applyandbindlet you set it yourself.- Arrow functions have no
this. They borrow it from where they were written, which makes them wrong as object methods and right as callbacks inside methods.