jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

Introducing this from five rules

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 given this 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 given this 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 its this 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's new works similarly to traditional class-based languages, but the internal mechanism is completely different. When we use new 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 keep this as undefined.

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 is undefined; if not, continue.
  • this points to the window 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 by UserA, so this in greet1 points to UserA, thus outputting UserA.
  • UserA.greet1.call(UserB)greet1 is called using .call, and the specified object is UserB, so it outputs UserB.
  • UserA.greet2()(): First, greet2 is called by UserA, returning a function without a bound context object, so the output is window.
  • User.greet2.call(UserB)(): Here, greet2 is called using .call specifying UserB, but it also returns a function without a bound context object, so the output is still window.
  • UserA.greet3()(): Here, the returned function is an arrow function with lexical binding, so the bound context object is UserA, thus outputting UserA.
  • UserA.greet3.call(UserB)(): Here, it also returns an arrow function, with the bound context object being UserB specified by .call, thus outputting UserB.

Reference links:

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.