preface

One of the most complex mechanisms in JavaScript is the This pointer, which even experienced JavaScript developers can’t necessarily tell you what it is

Literally, this seems to mean “here”, so we often think that this refers to the lexical scope in which it is located. But one day, you will find that the result is not our thinking, at that time, this is completely magic to us!

A small case

While we always think of this as a lexical scope pointing to itself, the following example is a bit of a surprise: why is result 0?

function foo(num) {
        console.log('foo:' + num); 
        this.count++;
    } 
    foo.count = 0; 
    for (var i = 0; i < 10; i++) {
        if (i > 5) { foo(i); }}// foo:6
    // foo:7
    // foo:8
    // foo:9 
    console.log(foo.count); / / 0?? <- why 0?
Copy the code

First, when I >5, we call foo(), and in foo() we print the number of times foo was called, and we increment the count variable by one. All the way to I =9, we’re done with the loop, and we print the count variable again. When foo. Count = 0, we do add an attribute count to the function object, so we traditionally think that this in the function refers to that count, so the final output should be 4. But why is the output 0 here?

The reason for this is that we misunderstood the direction of this, where the direction of this does not refer to the lexical scope of foo itself, but to the global scope. Since the count variable is not in the global scope, it creates it itself and initializes it as NAN. So each time the foo() function’s property count does not change at all, and the output is 0. The question then arises, why does it create a new variable in a global variable? It’s time to learn about engine lookup variables.

Recognize the engine’s lookup rules in scope

To master the this pointer problem, we must understand the engine’s lookup rules. Consider the following example

 / / 1
function foo(a) { / / 2
    var b = a * 2;

    function bar(c) { / / 3
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2); 
Copy the code

There are three hierarchically nested scopes in this example

  1. Contains the entire global scope and has an identifier foo
  2. Contains the scope created by Foo, with b, A, and bar identifiers
  3. Contains the scope created by bar, with a single C identifier

When the engine looks for the location of an identifier, the search always starts from the innermost scope and stops when the first matching identifier is found. In the code snippet above, the engine is executing consol.log(….) The declaration looks for references to variables A, B, and C, starting with the innermost scope, bar(…). The engine cannot find a, so it goes to bar(…). Upper level scope foo(…) Scoped lookup, this is finding a, so the engine uses this reference. B.

The code above clearly shows the engine’s search rules in scope, but what if the engine never finds a variable? Consider the following code

function foo(obj) {
    with (obj) {
        a = 2; }}var o1 = {
    a: 3
};
var o2 = {
    b: 3
}
foo(o1);
console.log(o1.a); / / 2

foo(o2);
console.log(o2.a); // undefined
console.log(a); / /!!!!!! Two is bad. Why is two in the global scope
Copy the code

When we pass o1 to with, with modifies the attribute A of O1. However, when o2 is passed to with, o2 has no attributes, so the engine looks for a variable layer by layer until it finds no a variable in the global scope, so it creates a variable in the global scope. This is known as the variable “leak” problem.

Deep call location

The this binding is actually bound when the function is called, and its orientation depends entirely on where the function is called. In general, analyzing where a function is called is analyzing the call stack. Call stack: all the functions that need to be called to get to the current location

function baz() {
    console.log('baz');
    bar();
}
function bar() {
    console.log('bar');
    foo();
}
function foo() {
    console.log('foo');
}
baz(); 
Copy the code

To call foo(), call bar(), and to call bar(), call baz(), so the call stack should be baz()->bar()->foo(). Here we can also use the built-in developer tools in the browser to view the call stack.

foo(...)

Remember the binding rules

Now that we know the function call stack, we know where the function is called, and then we need to decide which binding rules to apply. There are four binding rules

1. Default binding

function foo() {
    console.log(a);
}
var a = 2;
foo(); / / 2
Copy the code

Here this points to the global scope, because the function call is in the global scope, and calls like foo() that use an undecorated function reference directly can only use the default binding, which is our most common stand-alone function call.

2. Implicit binding

Implicit binding is something to consider when there are context objects around the call location, for example

function foo() {
    console.log(this.a);
}
var obj = {
    a:2.foo: foo
}
obj.foo(); / / 2
Copy the code

Implicit binding binds the function’s this to a context object when there is a context object around the call location. In other words, in this case, we called foo() in an obj object, which happens to be in an obj object, so the implicit binding binds this in the function to the obj object, so the output is the property A in the obj object.

A string of function calls has a function call stack, so what is a function contained in a string of objects? The answer is: object property chains.

function foo() {
    console.log(this.a);
};
var obj2 = {
    a: 1.foo: foo
};
var obj1 = {
    a: 2.obj2: obj2
};
obj1.obj2.foo(); / / 1
Copy the code

Here we can see that there are multiple objects, and if we understand the function call stack, the last element in the stack is where we call it, so the function is last called on obj1, this should be bound to obj1, and the output should be 2, but why is it 1?

Because only the first level in the object property chain is at work in the call location, in other words, the function call location is bound only in the first level object. Foo () was first called in obj2, so this was bound to obj2, and then foo in obj2 was called in obj1, but we don’t have to worry about this at this point because the binding for this is terminated at the first level.

Implicit loss has been mentioned in the Yellow Book (JavaScript you don’t know), and here is the definition

One of the most common problems with this binding is that implicitly bound functions will lose the binding object, meaning that it will apply the default binding to bind this to global objects or undefined.

For example

function foo() {
    console.log(this.a);
};
var obj = {
    a: 1.foo: foo
};
var bar = obj.foo; // The function is passed, the implicit binding is lost

var a = 'hello';

bar(); // 'hello' 
Copy the code

First we create a bar global variable and pass foo from obj to it. Since foo corresponds to a function, var bar = obj.foo equals foo(…). This function is passed to bar, and when we call bar again, it is an undecorated function call, so the default binding is applied, and the implicit binding is lost. What if we just want this to bind to obj? Here’s how to show bindings.

3. Display the binding

Show binding is easy to understand, we want this to bind to whatever object we want it to bind to using methods. There are three ways to do this. Note that once we show the binding we can’t bind anymore.

  • call(..)
  • apply(..)
  • bind(..)

call(..)
apply(..)
bind(...)

4. The new binding

To call a function using new, the first step is to create a brand new object. The second step is that this new object is bound to the function call this. Look at this case

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log(bar.a); / / 2
Copy the code

First, a new object bar is created when foo(..) is called with new. Function, we bind this in the bar object to foo(…) Function, so here a in bar refers to foo(..) A in the function, so the output is 2.

Binding priority

When we decide which of the four rules to apply, we’re pretty good. But there are times when it seems as if more than one rule can be applied to the call, and we need to consider the priority of the binding rules.

  • Implicit binding VS display binding
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 Displays the binding
obj2.foo.call(obj1); // 2 Displays the binding
Copy the code

From the above example we can see that the explicit binding takes precedence over the implicit binding because I can still use the explicit binding after the implicit binding.

  • New binding VS implicit binding
function foo(something) {
    this.a = something;
}
var obj1 = { 
    foo: foo
}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a);    / / 2
    
var bar = new obj1.foo(4);  
console.log(obj1.a); / / 2
console.log(bar.a); / / 4
Copy the code

From the example above we can see that the new binding takes precedence over the implicit binding because I can still use the new binding after the implicit binding.

  • New binding VS show binding
function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);    / / 2
    
var baz = new bar(3);  
console.log(obj1.a); // 2 ??
console.log(baz.a); / / 3
Copy the code

We would be surprised to find that the result of obj1.a is 2, not 3. After all, when we create the baz object, we re-give the bar(..) The value is assigned, then bar(..) Bound to obj1, the result should be 3. Well, when we create baz, we actually create a new object, and the this of the new object points to the this of the function call, so although the previous bar(..) Is hardbound to obj1, but the new binding modifies bar(..) This, which ultimately refers to foo(..) Function, so a in obj1 is not modified, and a new property is created in baz. From the example above we can see that the new binding takes precedence over the explicit binding because I can still use the new binding after the explicit binding.

The arrow function refers to this

Unlike normal functions that have multiple rules, the arrow function does not take into account the four binding rules. The arrow function => this is determined by its outer scope, as shown in the following example

var obj = {
    count: 0,
    cool: function() { console.log(this); / / object objsetTimeout(() => { console.log(this); // obj object this.count++; console.log("awesome?");
        }, 100);
    }
}
obj.cool();
Copy the code

In this example, the anonymous function “this” in obj refers to obj because it is called from inside the obj object. The built-in function setTimeout(..) The arrow function this points to its outer scope. The current scope of the arrow function is setTimeout(..). In, its upper scope is the scope of the anonymous function, so the arrow function’s this points to the anonymous function scope, and the result points to the obj object.

conclusion

Understanding the pointer to this is very important when we’re writing code, and while it can be a headache when the pointer to this is wrong, it’s an important building block for us as advanced front-end engineers!