'this' keyword in JavaScript: A Comprehensive Guide
When writing code in JavaScript, you may have come across the this
keyword - it refers to the current execution context or the value of the current object. It is a special keyword that is automatically defined in every function and can be used to reference the object that the function is a property of.
Bindings
The value of this
is determined by how a function is invoked. It can either be explicitly set or implicitly determined. There are four ways to call a function in JavaScript, and each way sets the value of this
differently:
Function invocation (default binding)
When a function is called as a standalone function, this
points to the global object. In Node.js, the global object is called global
:
function sayHello() {
return "Hello " + this.name;
}
sayHello(); // returns: 'Hello undefined'
global.name = "Charlie"
sayHello(); // returns: 'Hello Charlie'
NB: In a web browser, the global object is the window
object.
Method invocation (implicit binding)
When a function is called as a method of an object, this
points to the object that owns the method.
const person = {
name: 'Charlie',
sayHello: function() {
return "Hello " + this.name;
}
}
person.sayHello(); // returns: 'Hello Charlie'
Constructor invocation (constructor binding)
When a function is called with the new
keyword, this
points to the newly created object.
function Person(name) {
this.name = name;
this.sayHello = function() {
return "Hello " + this.name;
}
}
const charlie = new Person('Charlie');
charlie.sayHello(); // returns: 'Hello Charlie'
Indirect invocation (explicit binding)
When a function is called using the call()
or apply()
method, this
points to the object passed as the first argument to the method.
const person = {
name: 'Charlie'
}
function greet(timeOfDay) {
return `Good ${timeOfDay} ${this.name}`;
}
greet.call(person, 'Morning'); // returns: 'Good Morning Charlie'
apply
works in a similar fashion with the exception of having to pass an array of arguments:
const person = {
name: 'Charlie'
}
function greet(timeOfDay) {
return `Good ${timeOfDay} ${this.name}`;
}
greet.apply(person, ['Morning']); // returns: 'Good Morning Charlie'
Hard binding
It is also possible to explicitly set the value of this using the bind() method - this returns a new function with the value of this set to the object passed as the first argument.
const charlie = {
name: 'Charlie',
sayHello: function() {
return "Hello " + this.name;
}
}
charlie.sayHello(); // returns: 'Hello Charlie'
const paul = {
name: 'Paul'
}
const sayHelloToPaul = charlie.sayHello.bind(paul);
sayHelloToPaul(); // returns: 'Hello Paul'
Hard binding can be especially useful in situations where you need to pass a function as a callback but want to ensure that the this
value is always set correctly:
const btn = document.querySelector('button');
const person = {
name: 'Charlie',
greet: function() {
console.log(`Hello ${this.name}!`);
}
};
// Hard bind to person
btn.addEventListener('click', person.greet.bind(person));
Arrow Functions
Arrow functions do not have their own this
value and instead use the this
value of their lexical enclosing context. This makes them a useful alternative to traditional functions for avoiding the pitfalls of this
being reset.
Arrow functions lexically bind this
to the enclosing execution context:
const charlie = {
name: 'Charlie',
sayHello1: function() {
function greet(timeOfDay) {
console.log(`Good ${timeOfDay} ${this.name}`);
}
greet('Morning');
},
sayHello2: function() {
const greet = (timeOfDay) => {
console.log(`Good ${timeOfDay} ${this.name}`);
}
greet('Morning');
}
}
charlie.sayHello1(); // prints: 'Good Morning undefined'
charlie.sayHello2(); // prints: 'Good Morning Charlie'
Callback functions
Beware of the callback reset, even when using arrow functions:
function greet(timeOfDay) {
console.log(`Good ${timeOfDay} ${this.name}`);
}
const charlie = {
name: 'Charlie',
sayHello: function() {
greet('Morning');
}
}
charlie.sayHello(); // prints: 'Good Morning undefined'
Notice that this.name
inside greet
is undefined
. This is where hard binding comes useful:
function greet(timeOfDay) {
console.log(`Good ${timeOfDay} ${this.name}`);
}
const charlie = {
name: 'Charlie',
sayHello: function() {
greet.bind(this)('Morning');
}
}
charlie.sayHello(); // prints: 'Good Morning Charlie'
However, notice that bind(this)
doesn't solve the issue if arrow functions are used since, for it, the this
value is lexically bound, meaning it retains the this
value from its surrounding scope when declared, in our case the global
object:
const greet = (timeOfDay) => {
console.log(`Good ${timeOfDay} ${this.name}`);
}
const charlie = {
name: 'Charlie',
sayHello: function() {
greet.bind(this)('Morning');
}
}
charlie.sayHello(); // prints: 'Good Morning undefined'
To solve this, the context will have to be passed in:
const greet = (context, timeOfDay) => {
console.log(`Good ${timeOfDay} ${context.name}`);
}
const charlie = {
name: 'Charlie',
sayHello: function() {
greet(this, 'Morning');
}
}
charlie.sayHello(); // prints: 'Good Morning Charlie'
Take Away
Understanding how this
works in JavaScript is crucial for writing robust and maintainable code, especially when working with complex object-oriented programs. Here are some 5 key takeaways:
this
refers to the current execution context, in most cases, an object or the global scope.this
can be explicitly set using the.call()
,.apply()
, or.bind()
.- Arrow functions use the
this
value of their lexical enclosing context. - Beware of
this
being reset, sometimes to the global scope, in callback functions. To avoid this, use explicit binding or arrow functions to maintain thethis
value of the original context. - Avoid using
this
in certain situations when it makes our code harder to read and maintain, especially in larger applications.