Skip to content
academia.sh

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 this binding of their own; the this in their body is resolved lexically in the scope they are defined in.
  • call, apply, and bind cannot 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, or new.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.

Start typing to search.

↑↓ Esc navigate · open · close