Lexical this : How this works in Arrow Functions

Lexical this : How this works in Arrow Functions

·

11 min read

Traditional functions in JavaScript follow four common rules that dictate how this resolves during invocation. However, with the advent of ES6, arrow functions emerged as an alternative to traditional functions. They diverge from the standard this binding rules and instead adopt a lexical scoping mechanism, where this references the context in which the function was defined.

Let's delve deeper into this behaviour, explore common use cases, and gain a better understanding of how arrow functions operate.

Arrow Functions to the Rescue

Arrow functions, also denoted by the fat arrow operator , () => {}, were introduced in JavaScript to solve common problems with traditional function declarations and the this keyword. One major issue they address is the loss of the correct this binding when using regular functions as callbacks.

In arrow functions, this retains the value of the enclosing lexical context's this. In other words, when evaluating an arrow function's body, the language does not create a new this binding.

this - MDN

It’s sounds a bit counterintuitive that the function introduced to address this issue does not have its own this binding and this has led to developers creating a mental model that an arrow function essentially behaves like a hardbound function to the parent's this, though that is not accurate. The proper way to think about an arrow function is that it does not define a this keyword at all. In fact, there are several semantic differences and deliberate limitations in their usage:

  • Arrow functions are always anonymous. You can’t have named arrow functions like how you have named function expressions.

  • Arrow functions don't have their own bindings to this, arguments, or super, and should not be used as methods.

  • Arrow functions cannot be used as constructors. Calling them with new throws a TypeError.

Consider this excerpt from the EcmaScript Language Specification

An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function. Even though an ArrowFunction may contain references to super, the function object created in step 4 is not made into a method by performing MakeMethod. An ArrowFunction that references super is always contained within a non-ArrowFunction and the necessary state to implement super is accessible via the scope that is captured by the function object of the ArrowFunction.

ECMAScript® 2018 Language Specification

Lexical this

Arrow functions lack their own this binding. Therefore, if you use the this keyword inside an arrow function, it behaves just like any other variable. It will lexically resolve to an enclosing scope that defines a this keyword.

Lexical Binding

Arrow functions resolve this through lexical binding (lexical scoping). Lexical binding uses the location of where a variable is declared within the source code to determine where that variable is available.

Lexical binding determines the scope or context in which variables and functions are bound based on their location in the code — where they were declared in the code. In other words, lexical binding maps variables and functions with their respective scopes at the time of code compilation, rather than at runtime.

Let’s look at an example.

function outerFunction() {
  let outerVariable = 'outer';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const innerFunc = outerFunction();
innerFunc(); // 'outer'

Because, innerFunction() is defined within outerFunction(), it has access to the outerVariable defined in its outer scope. This access is possible due to lexical binding, where innerFunction() retains access to variables in its lexical environment, even when it's executed outside of its original scope.

Now that we understand the concept of lexical binding, let's explore how arrow functions leverage this concept to handle this differently. As mentioned earlier, arrow functions capture their this value from the surrounding lexical scope rather than having their own this binding. This behaviour contrasts with traditional functions, where thisis determined dynamically at runtime, based on the function's call-site.

Let’s see this in practice.

const printCharacter = () => {
  return this.character + ' is a main character in this show';
};

const series = {
  title: 'House of Cards',
  character: 'Frank Underwood',
  printTitle() {
    return `The name of the series is ${this.title}`;
  },
  printCharacter: printCharacter,

  printDetails() {
    console.log(this); // logs the series object
    return `${this.printTitle()} and ${this.printCharacter()}`;
  },
};

series.printDetails();
// The name of the series is House of Cards and undefined
// is a main character in this show

In this code, printDetails() is bound to the series object, so when printTitle() is called within it, it correctly references the series object, yielding the expected result.

However, unlike regular functions, this in arrow functions is not determined by the function’s call-site, — printCharacter's call-site in this case. Arrow functions use this from the surrounding lexical scope where they were defined, not where they were called. This means that since printCharacter() was defined in the global scope, it’ll capture this from the global scope. In the global scope this points to the global object in non-strict mode and undefined otherwise. This is why undefined is logged when attempting to access this.character. There is no character property on the global object.

To address this issue, you might consider defining printCharacter() as a method within the series object to ensure it uses the correct this context. Let's explore whether this approach resolves the issue.

Arrow Functions as Methods

We might be tempted to fix our earlier example by defining the printCharacter() directly inside the series object. However, arrow function expressions should be reserved for non-method functions due to their lack of their own this binding. Simply put, we shouldn’t use them as methods.

Let’s explore why with a simpler example.

const movie = {
  title: 'The Gray Man',
  printTitle: () => {
    console.log(`The name of the movie is ${this.title}`);
  },
};

movie.printTitle(); // The name of the movie is undefined

Despite being called as a method of movie, the this context inside the arrow function does not directly refer to the movie object itself. Instead, it captures the this value from its surrounding lexical scope — where it was created, namely the global scope.

Attempting to access this.title inside the arrow function results in undefined, as it essentially attempts to access this on the global object. The global scope defines this as the global object, or undefined in strict mode.

It's important to note that the curly braces {} of the movie object do not create scope; only functions do.

Taking Advantage of Lexical Binding

Referring back to our earlier example, do you think defining printCharacter() within the series object would make a difference? Take a second to consider it.

The answer is no. Declaring printCharacter() within the series object doesn’t change the outcome. This is because series is declared in the global scope, meaning printCharacter() is also defined in the global scope. In fact, disregarding scope, it's generally recommended to avoid using arrow functions for object methods.

To ensure that this points to the series object during invocation, let's explore two solutions to address this issue:

  1. Using a Regular Function

Instead of declaring printCharacter() as an arrow function, we can define it as a regular function since they have their own this binding that’s determined dynamically.

const series = {
    ...
  printCharacter() {
    return this.character + " is a main character in this show";
  },
  ...
};

series.printDetails();
// The name of the series is House of Cards and Frank Underwood
// is a main character in this show
  1. Lexical Binding

Alternatively, if we still prefer to use an arrow function, we can leverage lexical binding. By defining printCharacter() inside printDetails() — a normal function — it uses this from its surrounding scope. When printDetails() is invoked, it's bound to the series object, so this inside printCharacter() will also reference series.

const series = {
    ...

  printDetails() {
    const printCharacter = () => {
      return this.character + " is a main character in this show";
    };

    return `${this.printTitle()} and ${printCharacter()}`;
  },
};

series.printDetails();
// The name of the series is House of Cards and Frank Underwood
// is a main character in this show

Arrow Functions as Callbacks

Callbacks are typically invoked with a this value of undefined when called directly without attachment to any object. In non-strict mode, this results in the value of this being set to the global object.

Consider this example

const todd = {
  character: 'Todd',
  sayHi: function () {
    setTimeout(function () {
      console.log(`Hi, I'm ${this.character}!`);
    }, 1000);
  },
};

todd.sayHi(); // Hi, I'm undefined!

After a second delay, "Hi, I'm undefined!" is logged. This is because callback functions are executed in a different context where this points to the global object. Really, what we’re doing is trying to access the character property on the global object.

Traditionally developers would solve this error with lexical scoping by capturing this outside the callback.

const todd = {
  character: 'Todd',
  sayHi: function () {
    const self = this; // lexical capture of `this`

    setTimeout(function () {
      console.log(`Hi, I'm ${self.character}!`);
    }, 1000);
  },
};

todd.sayHi(); // Hi, I'm Todd!

We could solve this issue by using the bind method, or use this is where arrow functions shine and provide a better way to leverage lexical scoping. Arrow functions implicitly do something like a more efficient version of function(){}.bind(this)

const bojack = {
  character: 'BoJack',
  sayHi: function () {
    setTimeout(() => {
      console.log(`Hi, I'm ${this.character}!`);
    }, 1000);
  },
};

bojack.sayHi(); // Hi, I'm BoJack!

The arrow function inside the setTimeout callback preserves the this context of the bojack object. Since the arrow function doesn't have its own binding and setTimeout (as a function call) doesn't create a binding itself, the this context of the outer method is used, which is the bojack object when sayHi() is invoked.

Arrow Functions as Constructors

The lexical binding of an arrow-function cannot be overridden , even with new! With traditional functions, hard bound functions (functions invoked with call(),apply() or bind()) could have their binding overridden with the new keyword.

However, arrow functions cannot be used as constructors and will throw an error when called with new. They also do not have a prototype property.

const Foo = () => {};

const foo = new Foo(); // TypeError: Foo is not a constructor
console.log('prototype' in Foo); // false

Explicit Binding with Arrow Functions

Explicit binding methods such as call(), apply(), and bind() allow us to precisely control the this context of a function call.

function foo() {
  console.log(this.character);
}

var character = 'Lucile Bluth';

const cast = {
  character: 'Malory Archer',
};

foo.call(cast); // Malory Archer

However, when invoking arrow functions using call(), bind(), or apply(), the thisArg parameter is ignored. Though, you can still pass other arguments using these methods. Arrow functions are always bound lexically, regardless of how it’s invoked.

Consider the following example:

const obj = {
  num: 100,
};

// setting "num" on the global object to show how it gets picked up.
var num = 42;

const add = (a, b, c) => this.num + a + b + c;

console.log(add.call(obj, 1, 2, 3)); // 48
console.log(add.apply(obj, [1, 2, 3])); // 48

const boundAdd = add.bind(obj);
console.log(boundAdd(1, 2, 3)); // 48

In this scenario, add() is defined in the global context, so regardless of how it's invoked with explicit binding methods, this references the global object where add() was initially declared.

Nested Functions

When arrow functions are created inside other functions, their this remains that of the enclosing lexical context. The value of this inside wrapper() will be set at runtime, at the call-site because it is a regular function. add() in turn, will capture this from wrapper().

const obj = {
  num: 100,
};

// setting "num" on the global object to show how it gets picked up.
var num = 42;

function wrapper() {
  const sum = (val) => this.num + val;

  console.log(sum(10)); // 52
}

// default binding
// called in the global scope without any context object
wrapper();

When wrapper() is called, it was called without any binding context so default binding takes precedence where this points to the global object in non-strict mode. sum() was created within wrapper() and captures this from its surrounding scope —- the wrapper() function. Therefore, this in sum() references the global object where num is 42.

const obj = {
  num: 100,
};

// setting "num" on the global object
var num = 42;

function wrapper() {
  const sum = (val) => this.num + val;

  console.log(sum(10)); //110
}

// explicit binding
wrapper.call(obj);

When wrapper() is invoked, it’s this context is explicitly set to obj , which sum() captures. Therefore, this is sum() references obj where num is 100.

Explicit Binding in Nested Functions

We can also try to explicitly bind a nested arrow function just to prove that they always resolve this lexically.

const obj = {
  num: 100,
};

// setting "num" on the global object
var num = 42;

function wrapper() {
  const sum = (val) => this.num + val;

  console.log(sum.call({ num: 20 }, 10)); // 110
}

// explicit binding
wrapper.call(obj);

As we mentioned before, the thisArg parameter is ignored. sum() still uses obj as it’s this context when wrapper() is invoked.

Use Cases

Arrow functions are great in certain scenarios. Let's explore some do’s and don’ts.

When to Use Arrow Functions

  • Preserving Surrounding Scope: Arrow functions are ideal when you want to maintain the this context of the surrounding scope.

  • Inside Callbacks: Arrow functions are useful inside callback functions, e.g. with timers or event handlers. Their lexical this binding make them well-suited for these scenarios.

When to Avoid Arrow Functions

There are a few cases where arrow function are not ideal.

  • Not as Constructors: Arrow functions lack their own this keyword, making them unsuitable for use as constructors. Trying to instantiate objects with arrow functions will result in errors.

  • Avoid Prototype Assignments: Using arrow functions for methods assigned to the prototype object (SomeConstructorFunction.prototype.function = () => {}) is discouraged.

  • Avoid as Object Methods: Be careful when using arrow functions as methods inside objects, especially if they rely on the this keyword. Arrow functions do not bind this to the object they are created in, unless they are defined within a regular function.

Conclusion

Arrow functions in JavaScript capture this from their surrounding lexical scope, ensuring consistent behaviour across different contexts. They are always bound lexically, regardless of how they’re invoked which prevents unexpected this binding issues.


Curious about JavaScript's this behaviour? Check out my previous articles where we explore this and how it behaves in different contexts.

this All Makes Sense Now

this or That? What this isn’t