Before we start the article, let's solve a problem:
window.name = "window";
function User(name) {
this.name = name;
this.greet1 = function() {
console.log(this.name);
};
this.greet2 = function() {
return function() {
console.log(this.name);
};
};
this.greet3 = function() {
return () => console.log(this.name);
};
}
const UserA = new User("UserA");
const UserB = new User("UserB");
UserA.greet1();
UserA.greet1.call(UserB);
UserA.greet2()();
UserA.greet2.call(UserB)();
UserA.greet3()();
UserA.greet3.call(UserB)();
You can try to write the answer yourself. If you want to check your answer, you can move directly to the end of the article, and we will also provide an analysis at the end.
This article will introduce this in JavaScript through the following five rules:
- Implicit Binding
- Explicit Binding
- new Binding
- Lexical Binding
- Default Binding
Implicit Binding#
First, let's look at a piece of code:
const user = {
name: 'Jeremy',
greet () {
console.log(`My name is ${this.name}`)
}
}
Let's call the greet method in the user object:
user.greet(); // My name is Jeremy
We can see that when we call the method greet through the user object, this in greet points to the user object. This is the key to implicit binding: When a function reference has a context object, implicit binding binds this in the function call to this context object, so here this.name is equivalent to user.name.
Let's expand this a bit:
const user = {
name: 'Jeremy',
greet () {
console.log(`My name is ${this.name}`)
},
son: {
name: 'lap',
greet () {
console.log(`My name is ${this.name}`)
}
}
}
Does the result of calling user.son.greet() meet your expectations?
Now let's rewrite the code:
function greet () {
console.log(`My name is ${this.name}`)
}
const user = {
name: 'Jeremy'
}
We have separated greet into an independent function. Now how do we make this in greet point to the user object?
Explicit Binding#
In JavaScript, every function has a method that allows you to achieve this (i.e., change the direction of this), which is call:
call()method calls a function with a giventhisvalue and provided arguments (list of arguments).
So we can call it like this:
greet.call(user)
This is the meaning of explicit binding, where we explicitly (using .call) specify the direction of this.
If we want to pass some parameters to greet, we need to use the remaining parameters of the call method:
function greet (l1, l2, l3) {
console.log(`My name is ${this.name} and I know ${l1}, ${l2} and ${3}`)
}
const user = {
name: 'Jeremy'
}
const languages = ['JavaScript', 'Java', 'PHP']
greet.call(user, languages[0], languages[1], languages[2]) // My name is Jeremy and I know JavaScript, Java and PHP
When we actually practice this code, we find it cumbersome to pass the languages array one by one. In this case, we have a better option: .apply:
apply()method calls a function with a giventhisvalue and provides the arguments as an array (or array-like object).
The only difference between .apply and .call is the way parameters are passed, so we can call it like this:
greet.apply(user, languages) // My name is Jeremy and I know JavaScript, Java and PHP
The last method to introduce is .bind:
bind()method creates a new function that, when called, has itsthiskeyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
.bind is similar to .call, but the difference is that .call is called immediately, while .bind returns a new function that can be called later:
const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // My name is Jeremy and I know JavaScript, Java and PHP
new Binding#
Let's look at a new piece of code:
function User (name) {
this.name = name
}
const me = new User('Jeremy')
console.log(me.name) // Jeremy
Using new to call User constructs a new object and binds it to this in the User call.
JavaScript'snewworks similarly to traditional class-based languages, but the internal mechanism is completely different. When we usenewto call a function, or when a constructor function call occurs, the following operations are performed:
- A new object is created
- This object is linked to the prototype
- This object is bound to
thisin the function call- If the function does not return another object, the new object is returned
Lexical Binding#
The four rules introduced above can cover all normal functions, but in ES6, a special function was introduced: arrow functions.
Arrow function expressions have a shorter syntax than function expressions and do not have their own this, arguments, super, or new.target. These function expressions are more suitable for places that originally required anonymous functions, and they cannot be used as constructors.
Arrow functions do not have their own this; instead, this is determined by the outer (function or global) scope.
Let's rewrite the code again:
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return function () {
console.log(this.name);
}
}
}
We returned a function in the greet method. When we try to call user.greet()(), the result is undefined.
The reason for this is that when we call the returned function, there is no bound context object (it defaults to window), so a straightforward idea is to use explicit binding. Let's change the code as follows:
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return function () {
console.log(this.name);
}.bind(this)
}
}
user.greet()() // Jeremy
What if we rewrite it using arrow functions?
const user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
return () => {
console.log(this.name);
}
}
}
user.greet()() // Jeremy
The this lookup rule for arrow functions is actually similar to variable lookup. Before ES6, we were using a nearly equivalent pattern:
var user = {
name: 'Jeremy',
languages: ['JavaScript', 'Java', 'PHP'],
greet () {
var self = this;
return function () {
console.log(self.name);
}
}
}
user.greet()() // Jeremy
It is best not to mix these two styles in the same function or program, as it will make the code harder to write and maintain.
Default Binding#
Finally, let's revisit this piece of code:
function greet () {
console.log(`My name is ${this.name}`)
}
const user = {
name: 'Jeremy'
}
What happens if we call greet directly?
greet() // My name is undefined
This leads us to our last rule. If we have neither implicit binding (object call), nor explicit binding (.call, .apply, .bind), or new binding, then JavaScript will default this to the window object (hence default binding is also called window binding):
window.name = 'window'
function greet () {
console.log(`My name is ${this.name}`)
}
const user = {
name: 'Jeremy'
}
greet() // window
In ES5, if you enable strict mode, then
JavaScriptwill keepthisasundefined.
Summary#
Let's summarize a process for determining the direction of this:
- First, look at where the function is called.
- Is the function called through an object (is the left side of
.an object)? If so,thispoints to this object; if not, continue. - Is the function called using
.call,.apply, or.bind? If so,thispoints to the specified context object; if not, continue. - Is the function called with the
newkeyword? If so,thispoints to the newly created object; if not, continue. - Is the function an arrow function? If so,
thispoints to the first non-arrow function's function outside the arrow function; if not, continue. - Is the execution environment in strict mode? If so,
thisisundefined; if not, continue. thispoints to thewindowobject.
Finally, returning to the question at the beginning of the article, here is the answer to the execution:
window.name = "window";
function User(name) {
this.name = name;
this.greet1 = function() {
console.log(this.name);
};
this.greet2 = function() {
return function() {
console.log(this.name);
};
};
this.greet3 = function() {
return () => console.log(this.name);
};
}
const UserA = new User("UserA");
const UserB = new User("UserB");
UserA.greet1(); // UserA
UserA.greet1.call(UserB); // UserB
UserA.greet2()(); // window
UserA.greet2.call(UserB)(); // window
UserA.greet3()(); // UserA
UserA.greet3.call(UserB)(); // UserB
UserA and UserB are constructed using new, so their corresponding name values are UserA and UserB.
UserA.greet1(): First,greet1is called byUserA, sothisingreet1points toUserA, thus outputtingUserA.UserA.greet1.call(UserB):greet1is called using.call, and the specified object isUserB, so it outputsUserB.UserA.greet2()(): First,greet2is called byUserA, returning a function without a bound context object, so the output iswindow.User.greet2.call(UserB)(): Here,greet2is called using.callspecifyingUserB, but it also returns a function without a bound context object, so the output is stillwindow.UserA.greet3()(): Here, the returned function is an arrow function with lexical binding, so the bound context object isUserA, thus outputtingUserA.UserA.greet3.call(UserB)(): Here, it also returns an arrow function, with the bound context object beingUserBspecified by.call, thus outputtingUserB.
Reference links: