Deep analysis of this pointing problem in JavaScript

  • 2021-07-22 08:46:15
  • OfStack

The this point question in JavaScript has been explained in many articles, and many people still ask it. Last week, two people on our development team ran into problems, so I had to extend my exchange on front-end build technology for half a time to discuss this.

Unlike many common languages, the this pointing in an JavaScript function is not determined at the time the function is defined, but at the time it is called. In other words, the way the function is called determines the this pointing.

In JavaScript, there are three common function calls: direct call, method call and new call. In addition, there are some special calling methods, such as binding functions to objects through bind (), calling through call (), apply () and so on. However, after es6 introduced the arrow function, when the arrow function is called, its this direction is different. The following is an analysis of this pointing in these cases.

Direct call

Call directly, that is, call by the function name (...). At this point, the this inside the function points to the global object, which is window in the browser and global in NodeJs.

Look at an example:


//  Simple compatible browsers and  NodeJs  Global object of 
const _global = typeof window === "undefined" ? global : window;
function test() {
  console.log(this === _global);  // true
}
test();  //  Direct call 

One point to note here is that direct call does not mean calling in global scope. In any scope, the way to call a function directly by its name (...) is called direct call. For example, the following example also calls directly


(function(_global) {
  //  Pass  IIFE  Limit scope 
  function test() {
    console.log(this === _global); // true
  }
  test();   //  Direct invocation in non-global scope 
})(typeof window === "undefined" ? global : window);

Effect of bind () on direct invocation

Another point to note is the effect of bind (). Function. prototype. bind () binds the current function to the specified object and returns a new function whose this always points to the bound object no matter how it is called. Let's look at examples:


const obj = {};
function test() {
  console.log(this === obj);
}
const testObj = test.bind(obj);
test();   // false
testObj(); // true

So what did bind () do? Simulate an bind () to see how it affects the this.


const obj = {};
function test() {
  console.log(this === obj);
}
//  Custom function, simulation  bind()  Right  this  The influence of 
function myBind(func, target) {
  return function() {
    return func.apply(target, arguments);
  };
}
const testObj = myBind(test, obj);
test();   // false
testObj(); // true

As you can see from the above example, first of all, target, that is, bound objects, is maintained through closures; Then, when the function is called, the apply method is used for the original function to specify the this of the function. Of course, the native bind () implementation may be different and more efficient. But this example shows the feasibility of bind ().

Effect of call and apply on this

Function. prototype. apply () is used in the above example, as is Function. prototype. call (). For the usage of these two methods, please look at the documents by yourself through links. However, their first argument specifies that the this point in the function when it is run.

However, when using apply and call, it should be noted that if the directory function itself is a function bound to an this object, apply and call will not execute as expected, such as


const obj = {};
function test() {
  console.log(this === obj);
}
//  Bind to 1 A new object instead of  obj
const testObj = test.bind({});
test.apply(obj);  // true
//  Expectation  this  Yes  obj That is, output  true
//  But because  testObj  It's bound. It's not  obj  The object of, so it will output  false
testObj.apply(obj); // false

It can be seen that bind () has a far-reaching influence on functions, so use it with caution!

Method invocation

A method call is a call to a method function through an object, which is a form of call such as an object. Method function (...). In this case, this in the function points to the object calling the method. However, it is also necessary to pay attention to the influence of bind ().


const obj = {
  //  No. 1 1 A way to define an object's methods when you define it 
  test() {
    console.log(this === obj);
  }
};
//  No. 1 2 A way to attach an object after it is defined 1 Methods ( Function expression )
obj.test2 = function() {
  console.log(this === obj);
};
//  No. 1 3 The way and the first 2 The principle of each way is the same 
//  Is an object that is attached to it after it is defined 1 Methods ( Function definition )
function t() {
  console.log(this === obj);
}
obj.test3 = t;
//  This is also attached to the object 1 Method function 
//  But this function binds the 1 No  obj  Other objects of 
obj.test4 = (function() {
  console.log(this === obj);
}).bind({});
obj.test();   // true
obj.test2();  // true
obj.test3();  // true
//  Receive  bind()  Impact, test4  In  this  Point to not  obj
obj.test4();  // false

It should be noted here that the last three ways are to predefine functions and then attach them to obj objects as their methods. Again, the this pointing inside the function is independent of the definition and is influenced by the way it is called.

The case where this in the method points to a global object

Note that this is in a method, not in a method call. The this in the method points to the global object. If it is not because of bind (), then it must be because of the method call method that is not used, such as


const obj = {
  test() {
    console.log(this === obj);
  }
};
const t = obj.test;
t();  // false

t is the test method of obj, but when t () is called, the this in it points to the global.

The main reason why this situation is highlighted is that it is common to pass an object method to a function as a callback, only to find that it does not run as expected-because the influence of calling mode on this is ignored. For example, the following example is a problem that is particularly easy to encounter after encapsulating something in a page:


class Handlers {
  //  Here  $button  The assumption is that 1 Object that points to a button  jQuery  Object 
  constructor(data, $button) {
    this.data = data;
    $button.on("click", this.onButtonClick);
  }
  onButtonClick(e) {
    console.log(this.data);
  }
}
const handlers = new Handlers("string data", $("#someButton"));
//  Right  #someButton  After the click operation, 
//  Output  undefined
//  But the expected output  string data
 Apparently  this.onButtonClick  As 1 Parameters passed in  on()  After that, when the event is triggered, it is a direct call to this function, not a method call, so the  this  Points to a global object. There are many ways to solve this problem 
//  This is in  es5  The solution in 1
var _this = this;
$button.on("click", function() {
  _this.onButtonClick();
});
//  It can also be passed through  bind()  To solve 
$button.on("click", this.onButtonClick.bind(this));
// es6  Can be handled by the arrow function, in the  jQuery  Use with caution 
$button.on("click", e => this.onButtonClick(e));

Note, however, that using the arrow function as a callback to jQuery causes caution about the use of this within the function. jQuery this in most callback functions (non-arrow functions) represents the calling target, so you can write a statement like $(this). text (), but jQuery can't change the this direction of the arrow function, and the semantics of the same statement is completely different.

new call

Before es6, every 1 function could be treated as a constructor, and a new object could be generated by new call (without a specific return value within the function). es6 changes this state. Although the class defined by class still gets "function" with typeof operator, it cannot be called directly like ordinary function 1; At the same time, the method functions defined in class cannot be called with new as constructors.

In es5, calling a constructor with new creates a new object, and this points to the new object. There is no suspense, because new itself is designed to create new objects.


var data = "Hi";  //  Global variable 
function AClass(data) {
  this.data = data;
}
var a = new AClass("Hello World");
console.log(a.data);  // Hello World
console.log(data);   // Hi
var b = new AClass("Hello World");
console.log(a === b);  // false

this in Arrow Function

Let's take a look at the description of the arrow function on MDN first

An arrow function expression has a shorter syntax than a function expression and does not bind its own this , arguments , super , or new.target . Arrow functions are always anonymous. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

As clearly stated here, the arrow function does not have its own this binding. The this used in the arrow function is actually the this in the function or function expression that directly contains it. For example


const obj = {
  test() {
    const arrow = () => {
      //  Here's  this  Yes  test()  In  this , 
      //  By  test()  Determines how to call the 
      console.log(this === obj);
    };
    arrow();
  },
  getArrow() {
    return () => {
      //  Here's  this  Yes  getArrow()  In  this , 
      //  By  getArrow()  Determines how to call the 
      console.log(this === obj);
    };
  }
};
obj.test();   // true
const arrow = obj.getArrow();
arrow();    // true

Both this in the example are determined by the direct outer function (method) of the arrow function, while this in the method function is determined by how it is called. The invocation methods in the above examples are all method invocations, so this points to the object of the method invocation, that is, obj.

The arrow function makes it unnecessary for everyone to dwell too much on this when using closures, and does not need to temporarily reference this to closure functions through local variables like _ this. Looking at the translation of arrow function in paragraph 1 Babel may deepen our understanding:


(function(_global) {
  //  Pass  IIFE  Limit scope 
  function test() {
    console.log(this === _global); // true
  }
  test();   //  Direct invocation in non-global scope 
})(typeof window === "undefined" ? global : window);
0

Also note that the arrow function cannot be called with new or bind () to an object (although the bind () method call is fine, it does not produce the desired effect). Whenever you use an arrow function, it is not bound to this itself, it uses an this bound to a direct outer function (that is, the nearest Tier 1 function or function expression that contains it).


Related articles: