JAVASCRIPT / SCOPE, CLOSURES, AND THIS
Arrow functions and lexical this
Predict what `this` is inside any arrow function, use arrows to keep `this` in callbacks, and recognise when an ordinary function is required.
What you will learn
- Predict an arrow's `this` by reading outward through the scope chain, not the call site
- Replace `const self = this` and `.bind(this)` callbacks with arrow functions
- Explain why call, apply, bind, and thisArg parameters cannot retarget an arrow
- Choose ordinary functions for object and prototype methods that need dynamic `this`
Understanding Arrow functions and lexical this
An ordinary function gets a fresh `this` on every call, and the call site decides its value. An arrow function never creates that binding at all: its environment has no `this` slot, so when the body mentions `this` the engine walks outward through the scope chain, exactly as it would for a variable name, until it reaches a function that does have one. That single difference explains every arrow behaviour you will meet: the value depends on where the arrow was written, and a call site has nothing to fill in.
"Captured at definition time" is a useful shorthand, but the accurate model is that an arrow closes over the enclosing function's `this` the same way it closes over a `let`. If the enclosing ordinary function runs twice with two different receivers, each run produces a new arrow that reads that run's `this`. So an arrow written inside a method sees whichever object the method was called on, while an arrow written at the top level of a module sees `undefined`, because that is what the surrounding scope holds.
This makes arrows the right tool anywhere you previously wrote `const self = this` or `.bind(this)`: array callbacks, `.then` handlers, `setTimeout`, and per-instance event handlers. They are the wrong tool whenever a function must receive `this` from its caller, which covers object literal methods, prototype methods, class methods you want shared and overridable, and DOM handlers where you expect `this` to be the element. Since an arrow has no `this` of its own, it also has no `prototype` and cannot be called with `new`.
const panel = {
name: 'settings',
items: ['theme', 'font'],
labels() {
const fromArrow = () => this.name;
console.log(fromArrow());
console.log(fromArrow.call({ name: 'other' }));
return this.items.map((item) => this.name + '.' + item).join(' ');
}
};
console.log(panel.labels());
const backup = { name: 'backup', items: ['log'], labels: panel.labels };
console.log(backup.labels());An arrow function has no `this` binding of its own, so `this` inside it is resolved lexically through the scope chain like any other variable and cannot be changed by how the arrow is called.
Worked examples
Nested plain function versus nested arrow
Shows that only the arrow keeps the method's `this` when it is called as a bare function.
const clock = {
zone: 'UTC',
report() {
function plain() {
return this === clock;
}
const arrow = () => this === clock;
console.log('plain:', plain());
console.log('arrow:', arrow());
}
};
clock.report();Example explained
Line 1`plain()` is invoked with no receiver, so its own `this` is `undefined` in strict code and the global object otherwise, never `clock`.
Line 2Being written inside `report` gives `plain` access to `report`'s variables but not to its `this`, because `this` is not a variable for ordinary functions.
Line 3`arrow` has no `this` slot, so `this` resolves one scope out to `report`'s `this`, which is `clock` for the call `clock.report()`.
Arrow class field as a detachable handler
Demonstrates why a callback that will be stored and called later is safer as an arrow field than as a method reference.
class Button {
constructor(label) {
this.label = label;
this.presses = 0;
}
handleMethod() {
this.presses += 1;
}
handleArrow = () => {
this.presses += 1;
};
}
function fireTwice(handler) {
handler();
handler();
}
const b = new Button('save');
fireTwice(b.handleArrow);
console.log(b.presses);
try {
fireTwice(b.handleMethod);
} catch (err) {
console.log(err.name);
}Example explained
Line 1The `handleArrow` field initialiser runs during construction with `this` set to the new instance, so the arrow it creates reads that instance forever.
Line 2`fireTwice(b.handleArrow)` passes only the function value, yet the arrow still increments `b.presses`, giving 2.
Line 3`b.handleMethod` loses its receiver the same way, but as an ordinary method its `this` is `undefined` (class bodies are always strict), so `this.presses` throws a TypeError.
An arrow cannot be retargeted or constructed
Confirms that bind is silently ineffective on an arrow and that arrows have no prototype to construct from.
const tracker = {
id: 1,
make() {
return (...args) => this.id + ':' + args.length;
}
};
const fn = tracker.make();
console.log(fn(7, 8));
console.log(fn.bind({ id: 99 })(7, 8));
console.log(fn.hasOwnProperty('prototype'));
try {
new fn();
} catch (err) {
console.log(err.name);
}Example explained
Line 1`fn` was created while `make` ran with `tracker` as receiver, so `this.id` is 1 no matter who calls it.
Line 2`bind` returns a wrapper that sets a `this` value, but the arrow has no `this` slot to receive it, so the `{ id: 99 }` argument is discarded without error.
Line 3Arrows are defined without a `prototype` property, which is why `hasOwnProperty('prototype')` is false and `new fn()` throws instead of building an object.
Line 4`(...args)` is used because an arrow has no own `arguments` object either; `arguments` inside it would come from the enclosing function.
Arrow at top level of an object literal
Shows that an arrow used as an object method looks up `this` outside the object, not at it.
const outer = this;
const store = {
items: [],
addArrow: (x) => outer === this,
addMethod(x) {
return this === store;
}
};
console.log(store.addArrow('pen'));
console.log(store.addMethod('pen'));Example explained
Line 1`store.addArrow('pen')` is called with `store` as receiver, yet its `this` is still the surrounding script or module `this` captured where the arrow was written.
Line 2That surrounding value is whatever `outer` holds, so the comparison is true and `store` is unreachable from inside the arrow.
Line 3`addMethod` is an ordinary method, so its `this` comes from the call `store.addMethod(...)` and correctly refers to `store`.
Important notes
Arrows also lack their own `arguments`, `super`, `new.target`, and `prototype`, so use rest parameters in place of `arguments` and never call an arrow with `new`.
An arrow stored as a class field lives on each instance rather than on the prototype, so it costs one function per object and a subclass cannot replace it with a prototype method, because the own field always wins.
Common mistakes
Writing an object literal method as an arrow (`total: () => this.items.length`): `this` is the module or script `this`, so the property read either throws a TypeError in a module or silently returns `undefined` from the global object.
Trying to fix a lost `this` by calling `arrow.bind(obj)` or passing a thisArg to `forEach`: both are accepted and ignored, so the value never changes and the bug appears immune to the fix.
Using an arrow as a DOM event handler and expecting `this` to be the element: `this` stays whatever the enclosing scope had, and you must read `event.currentTarget` instead.
Try it yourself
Change, predict, then run
In a browser console create `const cart = { items: ['pen', 'ink'], sum() { /* ... */ } }` where `sum` loops with `this.items.forEach`, first logging `this === cart` from a `function` callback and then from an arrow callback. Then grab the arrow into a variable and call it through `.call({ items: [] })` to confirm the logged value does not change.
Open the JavaScript workspaceCheck your understanding
Given: const obj = { n: 1, make() { return () => this.n; } }; const f = obj.make(); const g = obj.make.call({ n: 2 }); console.log(f(), g.call({ n: 3 })); What is logged?
- 1 3
- 1 2
- 2 3
- undefined undefined
Show answer
`make` is an ordinary function, so each invocation has its own `this`: the first has `obj`, the second has `{ n: 2 }`, and the arrow returned by each one reads the `this` of the call that created it, giving 1 and 2. `1 3` is tempting because `g.call({ n: 3 })` looks like it retargets `g`, but an arrow has no `this` binding for `call` to set, so that argument is ignored.