Lexical this: How this works in Arrow Functions
#this, #javascript, #arrow functions, #lexical this, #scope
May 06, 202413 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'sthis
. In other words, when evaluating an arrow function's body, the language does not create a newthis
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
, orsuper
, and should not be used as methods. - Arrow functions cannot be used as constructors. Calling them with
new
throws aTypeError
.
Consider this excerpt from the EcmaScript Language Specification
An ArrowFunction does not define local bindings for
arguments
,super
,this
, ornew.target
. Any reference toarguments
,super
,this
, ornew.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 tosuper
, the function object created in step 4 is not made into a method by performing MakeMethod. An ArrowFunction that referencessuper
is always contained within a non-ArrowFunction and the necessary state to implementsuper
is accessible via the scope that is captured by the function object of the ArrowFunction.
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 this
is 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:
- 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
- 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 bindthis
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.
Back to Blogs