Is this (keyword) a problem?
Nikolas ⚡️
Posted on April 5, 2023
One of the trickiest part of Javascript language is this
mechanism. It is used to refer to the object that is currently executing the code.
It is not surprising that questions about this
are the part of most coding interviews. Let's dive into it step by step. At the end
I'll show you great set of questions that will help you to evaluate the value of this in any context.
this in global Context
At first, let's look at the global context.
// in browser
this === global // true
Top-level this
always points to the global object. It is worth to mention that this behavior is the same both for strict mode and non-strict mode.
// in node
this === global // true - only within the node REPL
// when excecuting the same code within a node module
this === module.exports // true
this in function calls
Usually the value of a function's this
is determined by the context in which the function is called. That means the this
value may be different each time the function is called.
function simpleCall() {
console.log(this); // global object
}
If we are in strict mode, the value of this
is undefined
in the global context.
"use strict";
function simpleCall() {
console.log(this); // undefined
}
With that knowledge let's assign a property to the global object and check the value of this
in the function call one more time:
function simpleCall() {
this.name = "John";
return this.name;
}
simpleCall(); // John
This could be a problem in some cases. The following examples shows how we can assign properties to the global object without any control.
The way to avoid this is to enable a strict mode. That prevents us from accidentally creating global variables, which is a good thing:
function simpleCall() {
this.name = "John"; // Error: cannot set property 'name' of undefined
return this.name;
}
simpleCall(); // Error: cannot set property 'name' of undefined
this in constructor calls
Here's a simple example of Coffee
function that accepts and initializes two properties:
function Coffee(name, price) {
this.name = name;
this.price = price;
}
const coffee = new Coffee("Cappuccino", 2.5);
console.log(coffee) // undefined
You should probably see the problem with the code above. this
within the function refers to global object because we are not in a strict mode.
Now we have accidentally created two global variables: name
and price
. Enabling a strict mode also won't solve this issue.
Since the this
is set to undefined we cannot assign properties to it - we will get an error - at least we didn't pollute the global object.
There is a little hint about the problem. The function name is uppercased, so we were supposed to use new
keyword to create an object:
function Coffee(name, price) {
console.log(this) // Coffee {}
this.name = name;
this.price = price;
}
const coffee = new Coffee("Cappuccino", 2.5);
console.log(coffee) // Coffee { name: 'Cappuccino', price: 2.5 }
console.log(global.name) // undefined
console.log(global.price) // undefined
In JavaScript, a function call with a new operator in front of it turns it into a constructor call.
When a function is invoked as a constructor, a brand-new object is created for us automatically, and that new object
is then used as the this
binding for the function call.
this in method calls
It’s common that an object method needs to access the information stored in the object to do its job. To access the object,
a method can use this
keyword.
To evaluate the value of this
in a method call, we need to know the context of the call.
The value of this
is the object "before dot", the one used to called the method.
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee() {
console.log(`Can I get ${this.name}, please?`); // Can I get Cappuccino, please? (this === coffee)
}
}
To better illustrate this case let's create the function ad assign it to two different objects:
const coffee = {price: 2.5,}
const tea = {price: 1.5}
function getPrice() {
console.log(this.price)
}
coffee.price = getPrice;
tea.price = getPrice;
// "this" inside the function is the object that called the method
coffee.price(); // 2.5 (this == coffee)
tea.price(); // 1.5 (this == tea)
It is a great moment demonstrate another case with a complex property chain like this below:
city.district.cafe.coffee.price(); // 2.5
We're still getting the same result. The receiver of the method call is still the most immediate property before the method - which is coffee
. The
city.district.cafe
prefix doesn't influence this
binding.
Real difficulty with understanding this
behavior in Javascript starts when a method loses its receiver. Going back to our example code:
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee() {
console.log(`Can I get ${this.name}, please?`); // Can I get Cappuccino, please? (this === coffee)
}
}
const yourOrder = coffee.orderCoffee;
yourOrder(); // Can I get undefined, please? (this === global)
I'm sure that you don't want to order undefined 😅. This outcome is caused by the fact that the method loses its receiver. The
function is called in global scope and we don't have the object "before dot" anymore.
We can came across a similar problem when using setTimeout
function. setTimeout
function is a method of the global object and
it will call our function with this
set to the global object. The first possible solution for this is wrapping the function
in a function that will set this
to the object that called the method. After this change coffee.orderCoffee
will be invoked
as a method.
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee() {
console.log(`Can I get ${this.name}, please?`);
}
}
setTimeout(function() {
coffee.orderCoffee(); // Can I get Cappuccino, please? (this === coffee)
}, 1000);
Using .bind(), .call() and .apply(),
Functions provide a built-in method bind
that allows to create a new function with a preset context. Here's the basic syntax:
const bounded = func.bind(context);
bounded
is a special function-like object, that is callable as function and passes the call to func
setting this = context
.
In other words bounded
is a function that will be called with this
set to context
.
Now let’s try with an object method:
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee() {
console.log(`Can I get ${this.name}, please?`);
}
}
setTimeout(coffee.orderCoffee.bind(coffee), 1000); // Can I get Cappuccino, please? (this === coffee)
const order = coffee.orderCoffee.bind(coffee);
// can be run withoud an object
order() // Can I get Cappuccino, please? (this === coffee)
bind
will create a new orderCoffee
function that will be called with this
permanently set to coffee
.
Now you can ask - what about .call() and .apply()?
The call()
method calls a function with a given this
value and arguments provided individually. The main differces between call
and bind
is that call
method:
- excecutes the function it was called on right away
- accepts additional parameters
- the
call
method does not make a copy of the function but uses the original one.
call
and apply
serve the same purpose. The only difference is that apply
accepts an array of arguments, whereas
call
accepts a comma-separated list of arguments.
function getOrder(coffee, size) {
console.log(`Customer: ${this.name} ordered: ${size} ${coffee}`);
}
const coffee = {
name: "Giovanni Giorgio",
}
getOrder.call(coffee, "cappuccino", "large"); // Customer: Giovanni Giorgio ordered: large cappuccino
getOrder.apply(coffee, ["cappuccino", "large"]); // Customer: Giovanni Giorgio ordered: large cappuccino
Arrow functions
Imagine that you have to forget about everything that you've learned so far and start from scratch. It is not fully true,
but behavior of this
in arrow functions is slightly different comparing to function declarations or function expressions.
An arrow function does not have its own this
binding. It is bound to the this
of the enclosing execution context.
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee: () => {
console.log(`Can I get ${this.name}, please?`);
}
}
coffee.orderCoffee(); // Can I get undefined, please? (this === global)
Assigning the property name
to the global object would solve this issue.
global.name = "Espresso"
const coffee = {
name: "Cappuccino",
price: 2.5,
orderCoffee: () => {
console.log(`Can I get ${this.name}, please?`);
}
}
coffee.orderCoffee(); // Can I get Espresso, please? (this === global)
As you can see to understand the value of this
in arrow functions we have to look to the first non-arrow function
scope. That is why arrow function from the example above will not display "Cappuccino" - it's because coffee
is an object
and not a function scope.
This knowledge could be extremely useful when you want to access this
within a callback. Let's leave the coffee related
examples for a while and take a look at the code below that uses setInterval
:
const counter = {
count: 0,
incrementCouter() {
setInterval(function() {
console.log(++this.count)
}, 1000)
}
}
counter.incrementCouter(); // NaN, NaN, NaN and so on...
It does not work. Why? this
binding within our function expression refers to the global object because that's how setInterval
works.
Since there is no count
property in the global object, it will not be able to access it. We're getting NaN
as a result
because incrementing ++undefined
returns NaN
.
Here is the solution:
const counter = {
count: 0,
incrementCouter() {
setInterval(() => {
console.log(++this.count)
}, 1000)
}
}
counter.incrementCouter(); // 1, 2, 3...
Important note. this
binding in arrow functions cannot be set explicitly. this
argument passed to an arrow function
using call()
, apply()
or bind()
is ignored. In other words, it doesn't matter how we call an arrow function.
this
argument will always refer to the this
value that was captured when the arrow function was created.
Summary
For the sake of simplicity, there is a list of questions that you should answer while evaluating the value of this
.
The order of the questions is really important.
is the function called with new?
Thenthis
refers to a constructor object.is the function called with call, apply or bind?
Thenthis
refers to the context passed as the argument call, apply or bind methodsis the function called as a method, ie: obj.method()?
Thenthis
refers to the object "before the dot" in the method callis the function called in the global scope?
Thenthis
refers to the global object, which iswindow
in browsers.
Posted on April 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024
November 24, 2024
November 8, 2024