This article is based on JavaScript you Don’t Know (Volume 1) by Kyle Simpson.

The first step in learning this is to understand that this refers neither to the function itself nor to the lexical scope of the function. This is actually a binding that happens when the function is called, and what it points to depends entirely on where the function is called.

Binding rules

1. Default binding

I’ll start with the most common type of function call: the stand-alone function call. You can think of this rule as the default rule when no other rule can be applied.

var a = 2;

function foo() {
  console.log(this.a);
  console.log(this= = =window); // true
}

foo(); / / 2
Copy the code

Looking at where foo() is called, you can see that in the code foo() is called directly with an undecorated function reference, so you can only use the default binding and no other rules can be applied. A variable declared in global scope (such as var a = 2) is an attribute of the global object. When foo() is called, this.a is resolved to the global variable a.

We will talk about this in strict mode. If you don’t understand strict mode, you can go to MDN first to see the syntax related to strict mode. Strict mode applies to entire scripts or to individual functions.

If strict mode is used, the default binding is not available for global objects, so this is bound to undefined:

function foo() {
  'use strict';
  console.log('this in foo'.this); // this in foo undefined
  console.log(this.a);
}

var a = 2;
console.log(this); // window
console.log(this.a); / / 2

foo(); // TypeError: this is undefined
Copy the code

Although the binding rules for this depend entirely on where it is called, the default binding can only be bound to a global object if foo() is not running in strict mode; Strictly independent of where foo() is called:

function foo() {
  console.log(this.a);
}

var a = 2;

(function () {
  'use strict';

  console.log(this); // undefined
  foo(); / / 2}) ();Copy the code

Generally speaking, you should not mix strict mode and non-strict mode in your code. The whole procedure is either strict or not.

2. Implicit binding

Another rule is whether the calling location has a context object, or is owned or contained by an object, although this can be misleading.

Consider the following code:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2.foo: foo,
};

obj.foo(); / / 2
Copy the code

The first thing to notice is how foo() is declared and then added to OBj as a reference property. But whether defined directly in OBj or defined first and then added as a reference property, this function is not strictly an OBj object. However, the call location references the function using the OBJ context, so you can say that the obJ object “owns” or “contains” the function when it is called. No matter what you call this pattern, when foo() is called, its foothold does point to an obj object. When a function reference has a context object, the implicit binding rule binds this in the function call to that context object. Because this is bound to obj when foo() is called, this.a and obj.a are the same. Only the top or last level in the object attribute reference chain affects the call location. For example:

function foo() {
  console.log(this.a);
}

var obj2 = {
  a: 42.foo: foo,
};

var obj1 = {
  a: 2.obj2: obj2,
};

obj1.obj2.foo(); / / 42
Copy the code

Implicit loss

One of the most common problems with this binding is that implicitly bound functions lose the binding object, meaning that it applies the default binding, binding this to global objects or undefined, depending on whether it is in strict mode.

Consider the following code:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2.foo: foo,
};

var bar = obj.foo; // Function alias!
var a = 'oops, global'; // a is an attribute of the global object

bar(); Oops, global "/ /"
Copy the code

Although bar is a reference to obj.foo, it actually refers to the foo function itself, so bar() at this point is really a function call without any decorations, so the default binding is applied.

A subtler, more common, and more unexpected situation occurs when a callback is passed in:

function foo() {
  console.log(this.a);
}

function doFoo(fn) {
  // fn actually refers to foo
  console.log(fn === foo); //true
  fn(); // <-- call location!
}

var obj = {
  a: 2.foo: foo,
};

var a = 'oops, global'; // a is an attribute of the global object

doFoo(obj.foo); // "oops, global"
Copy the code

Parameter passing is an implicit assignment, so when we pass in a function we are implicitly assigned. The fn argument in the function is a reference to foo, and foo() is an undecorated function call, so the result is the same as in the previous example.

What happens if you pass functions into the language’s built-in functions instead of your own declared functions? The result is the same, no difference:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2.foo: foo,
};

var a = 'oops, global'; // a is an attribute of the global object

setTimeout(obj.foo, 100); Oops, global "/ /"
Copy the code

As we have seen, it is quite common for callback functions to lose the this binding. In addition, there is another case where this behaves unexpectedly: the function that calls the callback may modify this. Event handlers in some popular JavaScript libraries often force the this of the callback function to be bound to the DOM element that triggered the event. This can be useful in some situations, but sometimes it can be very frustrating. Unfortunately, these tools often have no choice whether to enable this behavior.

In either case, the change to this is unexpected, and you actually have no control over how the callback function is executed, so there is no way to control where the call will affect the binding. We’ll show you how to fix this by fixing this later.

3. Explicit binding

When analyzing implicit binding, we must bind this indirectly (implicitly) to an object by including a property that refers to a function inside the object and by which the function is referred to indirectly. So what if we want to force a function call on an object instead of including a function reference inside the object?

You can use the call(..) of the function And the apply (..) Methods. Not familiar with the children’s shoes that use a method, can learn first.

Strictly speaking, JavaScript hosting environments sometimes provide very special functions that don’t have these two methods. However, such functions are very rare, and most of the functions provided by JavaScript and all the functions you create yourself can use Call (..). And the apply (..) Methods.

How do these two methods work? Their first argument is an object, which they bind to this, which they then specify when calling the function. Because you can specify the binding object for this directly, we call it an explicit binding.

Consider the following code:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2}; foo.call(obj);/ / 2
Copy the code

Through the foo. Call (..) , we can force foo to bind its this to obj when calling foo. If you pass a primitive value (String, Boolean, or number) as a binding object to this, the primitive value will be converted to its object form (i.e., new String(..)). , new Boolean (..) Or the new Number (..) ). Unfortunately, explicit binding still doesn’t solve the missing binding problem we mentioned earlier.

  1. Hard bindingA variant of explicit binding solves this problem.

Consider the following code:

function foo() {
  console.log(this.a);
}

var obj = {
  a: 2};var bar = function () {
  foo.call(obj);
};

bar(); / / 2
setTimeout(bar, 100); / / 2

// A hard-bound bar cannot change its this
bar.call(window); / / 2
Copy the code

Let’s see how this variant actually works. We created the function bar() and manually called foo.call(obj) inside it, forcing foo’s this to be bound to obj. No matter how bar is later called, it will always manually call foo on obj. This binding is explicitly forced and is therefore called hard binding.

A typical use of hard binding is to create a wrapped function that passes in all the arguments and returns all the values it receives:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

var obj = {
  a: 2};var bar = function () {
  return foo.apply(obj, arguments);
};

var b = bar(3); / / 2, 3
console.log(b); / / 5
Copy the code

Another way to use it is to create a helper function that can be reused:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

// A simple auxiliary binding function
function bind(fn, obj) {
  return function () {
    return fn.apply(obj, arguments);
  };
}

var obj = {
  a: 2};var bar = bind(foo, obj);

var b = bar(3); / / 2, 3
console.log(b); / / 5
Copy the code

Because hard binding is a very common pattern, there is a built-in function.prototype.bind method available in ES5, which can be used as follows:

function foo(something) {
  console.log(this.a, something);
  return this.a + something;
}

var obj = {
  a: 2};var bar = foo.bind(obj);

var b = bar(3); / / 2, 3
console.log(b); / / 5
Copy the code

bind(..) The bind() method creates a new function. When bind() is called, this of the new function is specified as the first argument to bind(), and the remaining arguments are used as arguments to the new function.

  1. The “context” of the API call

Many functions in third-party libraries, as well as many new built-in functions in the JavaScript language and host environments, provide an optional argument, often called a “context,” which serves the same purpose as bind(..) Also, make sure your callback uses the specified this.

For example :(don’t know children’s shoes, poke here)

function foo(el) {
  console.log(el, this.id);
}

var obj = {
  id: 'awesome'};/ / calls foo (..) Bind this to obj
[1.2.3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
Copy the code

These functions are actually called by call(..). Or apply (..) Explicit binding is implemented so you can write less code.

4. The new binding

In traditional class-oriented languages, “constructors” are special methods in a class that are called when a class is initialized with new. The usual format is this: JavaScript also has a new operator that looks the same as those used in class-oriented languages, and most developers assume that the new mechanism in JavaScript is the same as in those languages. However, the mechanics of new in JavaScript are actually quite different from class-oriented languages.

Let’s start by redefining “constructors” in JavaScript. In JavaScript, constructors are just functions that get called when you use the new operator. They do not belong to a class, nor do they instantiate a class. In fact, they are not even a special type of function; they are just ordinary functions called by the new operator.

For example, consider Number(..). Its behavior as a constructor is described in ES5.1 as follows:

15.7.2 Number ConstructorCopy the code

When Number is called in a new expression, it is a constructor: it initializes the newly created object.

So, all functions, including built-in object functions, can be called with new, which is called constructor calls. There is an important but very subtle difference: there really is no such thing as a “constructor,” only a “constructor call” to a function.

When a function is called with new, or when a constructor call occurs, the following operations are performed automatically.

  1. Create (or construct) a brand new object.
  2. The new object will be connected by the implementation [[prototype]].
  3. This new object is bound to the function call’s this.
  4. If the function returns no other object, the function call in the new expression automatically returns the new object.

Consider the following code:

function foo(a) {
  this.a = a;
}

var bar = new foo(2);

console.log(bar.a); / / 2
Copy the code

Call foo(..) with new , we construct a new object and bind it to foo(..). Call to this. New is the last way to influence the behavior of the this binding when a function is called, and we call it the new binding.

priority

Now that we know the four rules for the this binding in function calls, all you need to do is find where the function is called and decide which rule to apply. But what if more than one rule can be applied to a call location? To solve this problem, these rules must be prioritized, which is what we’ll cover next.

Without a doubt, the default binding has the lowest priority of the four rules, so we can leave it out for now.

Which has higher priority, implicit or explicit binding? Let’s test it out:

function foo() {
  console.log(this.a);
}

var obj1 = {
  a: 2.foo: foo,
};

var obj2 = {
  a: 3.foo: foo,
};

obj1.foo(); / / 2
obj2.foo(); / / 3

obj1.foo.call(obj2); / / 3
obj2.foo.call(obj1); / / 2
Copy the code

As you can see, explicit binding takes precedence, which means you should consider whether explicit binding can be applied first.

Now we need to figure out which of the new bindings has higher priority than the implicit binding:

function foo(something) {
  this.a = something;
}

var obj1 = {
  foo: foo,
};

var obj2 = {};

obj1.foo(2);
console.log(obj1.a); / / 2

obj1.foo.call(obj2, 3);
console.log(obj2.a); / / 3

var bar = new obj1.foo(4);
console.log(obj1.a); / / 2
console.log(bar.a); / / 4
Copy the code

You can see that the new binding takes precedence over the implicit binding. But which has higher priority, new binding or explicit binding?

function foo(something) {
  this.a = something;
}

var obj1 = {};

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); / / 2

var baz = newbar(3);
console.log(obj1.a); / / 2
console.log(baz.a); / / 3
Copy the code

Bar is hardbound to obj1, but new Bar (3) does not change obj1.a to 3 as we expected. Instead, new modifies the hard-bound (to obj1) call bar(..) In this. Because of the new binding, we get a new object named baz, and baz.a has a value of 3.

Here is a bind(..) provided by MDN. Implementation:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this! = ="function") {
      // Closest to ECMAScript 5
      // Internal IsCallable function
      throw new TypeError(
        "Function.prototype.bind - what is trying " +
        "to be bound is not callable"
      );
    }

    var aArgs = Array.prototype.slice.call(arguments.1),
      fToBind = this,
      fNOP = function () {
      },
      fBound = function () {
        return fToBind.apply(
          (
            this instanceof fNOP &&
            oThis ? this : oThis
          ),
          aArgs.concat(
            Array.prototype.slice.call(arguments)); }; fNOP.prototype =this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
  };
}
Copy the code

This code determines if the hard-bound function is called by new, and if so, replaces the hard-bound this with the newly created this.

To determine this

Now we can determine which rule is applied to a function at a particular call location based on priority. It can be judged in the following order:

  1. Is the function called in new (new binding)? If so, this binds to the newly created object.
  2. Is the function called by call, apply (explicit binding), or hard binding? If so, this binds to the specified object.
  3. Is the function called in a context object (implicitly bound)? If so, this binds to that context object.
  4. If neither, use the default binding. If in strict mode, it is bound to undefined, otherwise it is bound to global objects.

That’s it. With this in mind for normal function calls, you can understand how this bindings work. But… There should always be exceptions to the rule.

Bind the exception

The ignored this

If you pass null or undefined as a binding object for this to call, apply, or bind, these values will be ignored when called, and the default binding rules apply:

function foo() {
  console.log(this.a);
}

var a = 2;

foo.call(null); / / 2
Copy the code

A “safer” approach is to pass in a special object to which you can bind this without causing any side effects to your program. The easiest way to create an empty Object in JavaScript is object.create (null). Object.create(null) is similar to {}, but does not create the Object.prototype delegate, so it is “empty” than {}.

Indirect reference

Another thing to note is that it is possible (intentionally or unintentionally) to create an “indirect reference” to a function, in which case the default binding rules apply to calling the function.

Indirect references are most likely to occur in assignments:

function foo() {
  console.log(this.a);
}

var a = 2;
var o = { a: 3.foo: foo };
var p = { a: 4 };

o.foo(); / / 3
(p.foo = o.foo)(); / / 2
Copy the code

The return value of the assignment expression p.foo = o.foo is a reference to foo, so the call location is foo() rather than p.foo() or o.foo(). As we said earlier, the default binding is applied here.

Note: For the default binding, what determines the this binding object is not whether the calling position is in strict mode, but whether the function body is in strict mode. If the function body is in strict mode, this will be bound to undefined, otherwise this will be bound to the global object.

This lexical

Instead of using the function keyword, arrow functions are defined using () => {}. Instead of using the four standard rules for this, arrow functions determine this based on the outer (function or global) scope.

Let’s look at the lexical scope of the arrow function:

function foo() {
  Return an arrow function
  return (a) = > {
    // This inherits from foo()
    console.log(this.a);
  };
}

var obj1 = {
  a: 2};var obj2 = {
  a: 3};var bar = foo.call(obj1);
bar.call(obj2); // 2, not 3!
Copy the code

The arrow function created inside foo() captures foo() ‘s this when called. Since foo() ‘s this is bound to obj1, and bar (which references the arrow function)’ s this is bound to obj1, the binding of the arrow function cannot be modified. (Neither does New!)

Arrow functions are most commonly used in callback functions, such as event handlers or timers:

function foo() {
  setTimeout(() = > {
    // This in this method inherits from foo()
    console.log(this.a);
  }, 100);
}

var obj = {
  a: 2}; foo.call(obj);/ / 2
Copy the code

Arrow functions can look like bind(..) It also ensures that the function’s this is bound to the specified object, and is important because it replaces the traditional this mechanism with a more common lexical scope. In fact, we were using a pattern that was almost identical to the arrow function before ES6.

function foo() {
  var self = this; // lexical capture of this
  setTimeout(function () {
    console.log(self.a);
  }, 100);
}

var obj = {
  a: 2}; foo.call(obj);/ / 2
Copy the code

Although self = this and the arrow function both seem to replace bind(..) But essentially, they want to replace the This mechanism.

summary

If you want to determine the this binding of a running function, you need to find where the function is called directly. Once found, we can apply the following four rules in sequence to determine the object bound to this.

  1. Called by new? Bind to the newly created object.
  2. Called by call or apply (or bind)? Binds to the specified object.
  3. Called by a context object? Bind to that context object.
  4. Default: bind to undefined in strict mode, otherwise bind to global object.

It is important to note that some calls may inadvertently use the default binding rules. If you want to “safely” ignore this binding, you can use a DMZ Object such as ø = object.create (null) to protect global objects.

Instead of using the four standard binding rules, the arrow function in ES6 determines this based on the current lexical scope. Specifically, the arrow function inherits the this binding of the outer function call (whatever this is bound to). “This is actually the same self = this mechanism as in previous ES6 code.