Lesson 08 / 17
Context in Arrow Functions
Arrow functions having no context of their own, lexical resolution, the absence of a constructor and prototype, the cost of arrows defined as class fields, and cases of misuse.
Contents
In the previous lesson’s last example, the third fix for the callback problem was an
arrow function, and it required no binding operation at all. The reason is that an
arrow function does not take its this value from the call site.
Arrow functions have no this binding of their own. The this used in their body is
resolved like an ordinary variable: searched from the inside out, through the scope
chain of where it was defined. The lexical scope rule covered in the Programming
Foundations course’s Scope and Lifetime lesson applies here to this as well. This one
difference is why none of the previous lesson’s four rules apply to arrow functions.
A Function With No Context of Its Own
The shortest way to see the distinction is to place the two definition forms side by side on the same object.
const recordWithMethod = { sensor: "S-01", value: 21.4, format() { return `${this.sensor}: ${this.value}`; }, }; const recordWithArrow = { sensor: "S-02", value: 19.8, format: () => { return this === undefined ? "this: undefined" : "this: something else"; }, }; console.log(recordWithMethod.format()); console.log(recordWithArrow.format()); const brokenArrow = recordWithArrow.format; console.log(brokenArrow()); console.log(recordWithArrow.format.call(recordWithMethod));
S-01: 21.4 this: undefined this: undefined this: undefined
An object literal establishes no scope. Because the arrow function is defined in the
module body, its this value comes from there, and in the module body this value is
undefined. Calling it through the object changes nothing.
The last line settles the rule: even call cannot change an arrow function’s context.
Because there is no place to bind to, explicit binding silently has no effect — and
throws no error either. bind is just as ineffective in the same way; it produces a
new function but does not touch the inner resolution of this.
The Benefit of Lexical Resolution
Having no context of its own looks like a shortcoming; its real value shows up in
callbacks given from inside a method. The method’s this value is automatically
carried into an arrow function defined inside it.
class MeasurementSeries { constructor(sensor, values) { this.sensor = sensor; this.values = values; } labeledArrow() { return this.values.map((value) => `${this.sensor}:${value}`).join(" "); } labeledFunctionExpr() { return this.values .map(function (value) { return `${this.sensor}:${value}`; }) .join(" "); } labeledFallback() { const self = this; return this.values .map(function (value) { return `${self.sensor}:${value}`; }) .join(" "); } } const series = new MeasurementSeries("S-01", [21.4, 19.8]); console.log(series.labeledArrow()); try { console.log(series.labeledFunctionExpr()); } catch (error) { console.log(`function expression: ${error.constructor.name}`); } console.log(series.labeledFallback());
S-01:21.4 S-01:19.8 function expression: TypeError S-01:21.4 S-01:19.8
The function expression in the second method establishes its own this binding, and
because it is called as a callback, this binding falls to the default rule; in strict
mode it is undefined and the read throws an error. The third method copies the
context into an ordinary variable — this is the common fix from before arrow functions
entered the language, and it still works correctly. The first method does the same job
without an extra name.
The rule can be summarized as: if the caller provides no context, an arrow function is the right choice. Callbacks, functions given to array methods, and helper functions defined inside a method all fit this description.
The Other Things That Are Missing
this is not the only thing missing from arrow functions. The arguments object,
new.target, and the prototype property are not present either. The last of these
is the direct reason arrow functions cannot be used as constructors.
const arrowCalibrate = (value) => value * 1.02; function functionCalibrate(value) { return value * 1.02; } console.log("prototype" in arrowCalibrate); console.log("prototype" in functionCalibrate); console.log(typeof arrowCalibrate); try { new arrowCalibrate(1); } catch (error) { console.log(`new with arrow: ${error.constructor.name}`); } function outerFunction() { const innerArrow = () => arguments[0]; return innerArrow(); } console.log(outerFunction("outer-arg")); const withRest = (...args) => args.length; console.log(withRest(1, 2, 3)); console.log(arrowCalibrate.name); console.log(arrowCalibrate.length);
false true function new with arrow: TypeError outer-arg 3 arrowCalibrate 1
The second lesson wrote the first step of the new operator as “create an empty
object whose prototype is bound to the function’s prototype object.” Because no such
object exists for an arrow function, that step can never run at all; the error is a
consequence of this absence.
arguments is also resolved lexically: used inside an arrow function, it gives the
arguments of the function that wraps it. Rather than relying on this behavior, rest
parameters should be used; they work the same way in both arrow functions and regular
functions and produce a real array. The last two lines show that arrow functions still
carry ordinary function properties like name and parameter count.
Arrow Function as a Class Field
Assigning an arrow function to a field in a class body solves the binding problem definitively. Its cost is paid in the prototype model.
class MeasurementCollector { constructor(sensor) { this.sensor = sensor; this.values = []; } add(value) { this.values.push(value); return this.values.length; } addArrow = (value) => { this.values.push(value); return this.values.length; }; } const a = new MeasurementCollector("S-01"); const b = new MeasurementCollector("S-02"); console.log(Object.getOwnPropertyNames(MeasurementCollector.prototype).join(",")); console.log(Object.getOwnPropertyNames(a).join(",")); console.log(a.add === b.add); console.log(a.addArrow === b.addArrow); const detachedArrow = a.addArrow; console.log(detachedArrow(21.4)); console.log(a.values.join(",")); console.log(a.addArrow.call(b, 99)); console.log(a.values.join(",")); console.log(b.values.join(",") || "(empty)");
constructor,add addArrow,sensor,values true false 1 21.4 2 21.4,99 (empty)
The first two lines show the distinction: add sits on the prototype, addArrow is
every instance’s own property. The third and fourth lines measure the consequence —
the prototype’s method is the same function across every instance, the arrow field is
separate on every instance.
The fifth and sixth lines show the gain: the arrow field, detached from its object,
keeps writing to the correct collector. The last three lines are the other side of that
same trait — trying to redirect it to another collector with call has no effect; the
value is still written to object a.
The trade-off is clear. The arrow field guarantees context but produces one function
object per instance, removes sharing, and makes redirection impossible. The
prototype’s method is shared and redirectable but entrusts context to the call site.
The bind pattern in the previous lesson’s constructor is another spelling of this
same trade-off.
Where the Arrow Function Is Wrong
As a rule: if a function needs to reach its object through this, it cannot be an
arrow function. This covers methods in object literals and behavior written onto a
prototype.
const measurementBehavior = { format: () => `${this?.sensor}`, }; const record = Object.create(measurementBehavior); record.sensor = "S-01"; console.log(record.format()); const correctBehavior = { format() { return `${this.sensor}`; }, }; const correctRecord = Object.create(correctBehavior); correctRecord.sensor = "S-01"; console.log(correctRecord.format()); const counterFactory = () => { let count = 0; return { increment: () => (count += 1), read: () => count, }; }; const counter = counterFactory(); counter.increment(); counter.increment(); console.log(counter.read());
undefined S-01 2
The first record correctly has its prototype set, but the function in the chain reads
this not from the chain but from its own defining scope; the result is undefined.
Correct behavior is obtained just by changing the definition form.
The last section previews the second pattern where arrow functions are used correctly
in advance: when an object’s data is held in a closed-over variable instead of
this, this is never needed at all and the binding problem disappears. This setup is
called a closure and is the next lesson’s subject.
Summary
- Arrow functions have no
thisbinding of their own; thethisin their body is resolved lexically in the scope they are defined in. call,apply, andbindcannot change an arrow function’s context; they silently have no effect.- An arrow function automatically carries context in callbacks given from inside a method; it is the right choice when the caller provides no context.
- Arrow functions have no
prototype,arguments, ornew.target; this is why they cannot be used as constructors. - An arrow function defined as a class field produces one copy per instance: it guarantees context but loses sharing and redirectability.
- Methods in object literals and behavior written onto a prototype cannot be arrow functions.
Next Step
In the last example, a counter kept its state not in an object property but in a local variable of the function that produced it. That variable kept living even after the function returned, and was visible only to the functions returned from it. The next lesson covers this structure — closures; it examines the lifetime of closed-over variables and compares this way of hiding state with the prototype chain.
To keep your progress and take notes, Log in
My notes
Log in to take notes.