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 giventhis
value 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 giventhis
value 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 itsthis
keyword 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
'snew
works similarly to traditional class-based languages, but the internal mechanism is completely different. When we usenew
to 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
this
in 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
JavaScript
will keepthis
asundefined
.
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,this
points to this object; if not, continue. - Is the function called using
.call
,.apply
, or.bind
? If so,this
points to the specified context object; if not, continue. - Is the function called with the
new
keyword? If so,this
points to the newly created object; if not, continue. - Is the function an arrow function? If so,
this
points to the first non-arrow function's function outside the arrow function; if not, continue. - Is the execution environment in strict mode? If so,
this
isundefined
; if not, continue. this
points to thewindow
object.
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,greet1
is called byUserA
, sothis
ingreet1
points toUserA
, thus outputtingUserA
.UserA.greet1.call(UserB)
:greet1
is called using.call
, and the specified object isUserB
, so it outputsUserB
.UserA.greet2()()
: First,greet2
is called byUserA
, returning a function without a bound context object, so the output iswindow
.User.greet2.call(UserB)()
: Here,greet2
is called using.call
specifyingUserB
, 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 beingUserB
specified by.call
, thus outputtingUserB
.
Reference links: