JavaScript140 min total · 15 parts
JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop
Part 7 of 15 · ~6 min
this: How It Gets Bound
Our search box is about to change shape, and this is going to break. Let us do it in that order — break it first, then explain it — because the explanation lands much harder when you have already watched it happen.
Rewriting the widget as an object is a reasonable thing to want. It reads well, it groups related things, and it is what most codebases converge on eventually:
const searchBox = {
latestQuery: "",
isOpen: false,
open() {
this.isOpen = true;
listEl.hidden = false;
},
close() {
this.isOpen = false;
listEl.hidden = true;
},
};
searchBox.open(); // works perfectly
searchBox.close(); // works perfectly
Now wire close up to the thing that should trigger it — clicking anywhere outside the dropdown:
document.addEventListener("click", searchBox.close);
// TypeError: Cannot set properties of undefined (setting 'isOpen')
Same function. We did not edit it, wrap it, or copy it. We passed the identical function object to addEventListener, and it broke.
Here is the rule that explains it: for a regular function, this is decided by how the function is called, not by where it was written. Scope is lexical — determined by the code's shape, fixed forever when you type it. this is not. this is determined fresh at every call, by the syntax of the call site.
searchBox.close() is a call through searchBox, so this is searchBox. But addEventListener does not call it that way. It stashed the function and later called it plainly — handler(event) — with no object in front of the dot, because there is no dot. There is no searchBox anywhere in that call, so this is not searchBox.
This is also the cleanest way to see that a method is not "attached" to its object in any deep sense. searchBox.close is just a property holding a function. Read it out and you have a plain function with no memory of where you found it.
The four ways this gets determined
There are exactly four call-site patterns, and one escape hatch that opts out of the whole system. Every this question you will ever have is one of these five rows:
| Call pattern | this inside the function |
|---|---|
obj.method() | obj — the object to the left of the dot |
fn() (plain call) | undefined in strict mode, ES modules, and class bodies; the global object in sloppy mode |
fn.call(thisArg, ...) / fn.apply(thisArg, [...]) | Explicitly thisArg, for that one call |
new Fn() | The brand-new object being constructed |
| Arrow function | Not its own at all — inherited lexically from the enclosing scope, fixed at definition |
Read the first two rows together and our bug is fully explained. searchBox.close() matches row one. The call addEventListener makes internally matches row two. Row two in a module — which is everything you write today — gives you undefined, and undefined.isOpen = true is the TypeError we got.
One historical note that saves a lot of confusion when you meet older code: in sloppy mode, row two gives you the global object instead of undefined. So the same bug in a 2013 codebase does not throw. It silently sets window.isOpen = true and the dropdown just never opens, with no error anywhere. Strict mode turning that into a loud TypeError is a straightforward improvement.
Arrow functions are the exception
Our search box needs a loading indicator — a little ... that animates while results are being fetched. Written the obvious way, it does not work:
const searchBox = {
dots: 0,
startSpinner() {
setInterval(function () {
this.dots = (this.dots + 1) % 4; // `this` is NOT searchBox
spinnerEl.textContent = ".".repeat(this.dots);
}, 300);
},
};
Row two again. setInterval calls its callback plainly, so this inside that function is undefined, no matter that it is written inside a method of searchBox. Where the function sits in the source has never had any bearing on this.
Unless it is an arrow function, which is the one construct where it does:
const searchBox = {
dots: 0,
startSpinner() {
this.intervalId = setInterval(() => {
this.dots = (this.dots + 1) % 4; // `this` is searchBox — inherited from startSpinner
spinnerEl.textContent = ".".repeat(this.dots);
}, 300);
},
stopSpinner() {
clearInterval(this.intervalId);
},
};
There's no version of this that belongs to an arrow function. Calling it differently doesn't create one, and nothing — not call, not apply, not bind — can hand it one either. this inside an arrow function is looked up exactly the way any ordinary variable would be — out through the enclosing scopes until one has it. Here that search lands in startSpinner, where this is searchBox, and that is what the arrow sees.
Which reframes the whole thing usefully: this in an arrow function is just a closure over the enclosing this. It is not a second, parallel mechanism you have to learn. It is the closure rule you already know, applied to one more name.
This is also why arrow functions became the default for callbacks written inside methods. It is not a style preference — it is that the callback keeps the this you meant, without any extra work. And it is why call, apply, and bind have no effect on one: you cannot set something the function does not have.
call, apply, and bind
All three answer the same question — what if I want to choose this myself, instead of letting the call site decide? — and they differ only in when and how.
function describe(prefix) {
return `${prefix}: "${this.latestQuery}" (${this.isOpen ? "open" : "closed"})`;
}
const headerBox = { latestQuery: "closures", isOpen: true };
describe.call(headerBox, "header"); // "header: "closures" (open)" — runs now, args listed out
describe.apply(headerBox, ["header"]); // identical — runs now, args as an array
const describeHeader = describe.bind(headerBox);
describeHeader("header"); // runs later — bind returns a NEW permanently-bound function
call and apply invoke immediately and differ only in argument shape — call takes a comma-separated list, apply takes an array, which is as good a mnemonic as any. bind invokes nothing; it hands back a new function with this welded on permanently, which is exactly what you need when you are handing a function to somebody else to call later.
Somebody like addEventListener. Which is our original bug, and now we can fix it properly:
document.addEventListener("click", searchBox.close.bind(searchBox));
And in the class form the same problem shows up with a standard fix:
class SearchBox {
constructor(inputEl, listEl) {
this.isOpen = false;
this.close = this.close.bind(this); // bind once, in the constructor
document.addEventListener("click", this.close); // now safe to detach
}
close() {
this.isOpen = false;
}
}
One practical warning that costs people real debugging time: bind returns a new function object every time you call it. So this does not work —
document.addEventListener("click", searchBox.close.bind(searchBox));
document.removeEventListener("click", searchBox.close.bind(searchBox)); // removes nothing
— because the second bind produced a different function from the first, and removeEventListener matches by identity. Bind once, keep the result, use that same reference for both. Which is precisely why the class above binds in the constructor rather than at the point of use, and it ties straight back to the teardown problem from the closures chapter: a listener you cannot remove is a leak.