[JS pocket book] Chapter 8: this in JS from a more detailed perspective

Keywords: Javascript React JSON github Python

By valentinogagliardi
Translator: front-end wit
Source: github

Alibaba cloud has been doing activities recently, with a discount of 20%. If you are interested, please take a look at:
https://promotion.aliyun.com/...

In order to ensure readability, this paper adopts free translation instead of literal translation.

Uncover "this"

This keyword in JS is a puzzle for beginners and an eternal puzzle for experienced developers. This is actually a moving target, which may change during code execution without any obvious reason. First, take a look at what this keyword looks like in other programming languages.
Here is a Person class in JS:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("Hello " + this.name);
  }
}

Python class also has something similar to this, called self:

class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return 'Hello' + self.name

In Python classes, self represents an instance of a class: a new object created from the class

me = Person('Valentino')

There are similar things in PHP:

class Person {
    public $name; 

    public function __construct($name){
        $this->name = $name;
    }

    public function greet(){
        echo 'Hello ' . $this->name;
    }
 }

Here $this is the class instance. Use JS class again to create two new objects. You can see that when we call object.name, we will return the correct name:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log("Hello " + this.name);
  }
}

const me = new Person("Front-end little intelligence");
console.log(me.name); // 'front end wit'

const you = new Person("Little intelligence");
console.log(you.name); // 'little wisdom'

JS is similar to Python, Java and PHP, because this seems to point to the actual class instance?

This is not right. Let's not forget that JS is not an object-oriented language, and it is loose, dynamic, and has no real classes. This has nothing to do with classes. Let's use a simple JS function (try browser) to prove this:

function whoIsThis() {
  console.log(this);
}

whoIsThis();

Rule 1: go back to the global "this" (default binding)

If you run the following code in the browser

function whoIsThis() {
  console.log(this);
}

whoIsThis();

The output is as follows:

Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, ...}

As shown above, when this is not in any class, this still has a value. When a function is called in the global environment, it will point its this to the global object, in our case, window.

This is the first rule of JS, called the default binding. The default binding is like a fallback, which is not popular in most cases. Any function running in a global environment can "pollute" global variables and corrupt code. Consider the following code:

function firstDev() {
  window.globalSum = function(a, b) {
    return a + b;
  };
}

function nastyDev() {
  window.globalSum = null;
}

firstDev();
nastyDev();
var result = firstDev();
console.log(result);

// Output: undefined

The first developer creates a global variable called globalSum and assigns it a function. Next, another developer assigns null to the same variable, causing the code to fail.

There is always a risk in dealing with global variables, so JS introduces "safe mode": strict mode. Strict mode is enabled by using use Strict. One of the benefits of strict mode is the elimination of the default binding. In strict mode, when you try to access this from the global context, you get undefined.

"use strict";

function whoIsThis() {
  console.log(this);
}

whoIsThis();

// Output: undefined

Strict patterns make JS code more secure.

To summarize, the default binding is the first rule in JS: when the engine cannot find out what this is, it will return to the global object. Next let's look at three other rules.

Rule 2: when "this" is the host object (implicit binding)

Implicit binding is a daunting term, but the theory behind it is not so complicated. It narrows down to objects.

var widget = {
  items: ["a", "b", "c"],
  printItems: function() {
    console.log(this.items);
  }
};

When a function is assigned a property of an object, the object becomes the host of the function. In other words, this in the function will automatically point to the object. This is the second rule in JS called implicit binding. Implicit binding is also working even when calling functions in the global context.

function whoIsThis() {
  console.log(this);
}

whoIsThis();

We can't see from the code, but the JS engine assigns this function to a new property on the global object window, as follows:

window.whoIsThis = function() {
  console.log(this);
};

We can easily confirm this hypothesis. Run the following code in the browser:

function whoIsThis() {
  console.log(this);
}

console.log(typeof window.whoIsThis)

Print function. At this point you may ask: what is the real rule of this in global functions?

It's like a default binding, but it's actually more like an implicit binding. It's a bit confusing, but just remember that the JS engine always returns this globally when it's unable to determine the context (the default binding). On the other hand, when a function is called as part of an object, this points to the called object (implicitly bound).

Rule 3: display the specified "this" (that is, explicit binding)

If you are not a JS user, it is difficult to see such code:

someObject.call(anotherObject);
Someobject.prototype.someMethod.apply(someOtherObject);

This is explicit binding. In React, you will often see the binding method:

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    // bounded method
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

Now React Hooks makes classes almost unnecessary, but there are still many "legacy" React components that use ES6 classes. One of the questions most beginners will ask is, why do we rebind event handler methods through bind 'method in React?

call, apply and bind all belong to Function.prototype. Explicit binding for (Rule 3): explicit binding refers to explicitly binding this to a context. But why explicitly bind or rebind functions? Consider some legacy JS code:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

showModal is the "method" bound to the object legacyWidget. this.html is hard coded, and the created element is written to death (div). So we can't attach the content to the label we want to attach.

The solution is to explicitly bind this to change the object of showModal.. Now, we can create a widget and provide a different HTML element as an additional object:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

var shinyNewWidget = {
  html: "",
  init: function() {
    // A different HTML element
    this.html = document.createElement("section");
  }
};

Next, call the original method:

var legacyWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("div");
  },
  showModal: function(htmlElement) {
    var newElement = document.createElement(htmlElement);
    this.html.appendChild(newElement);
    window.document.body.appendChild(this.html);
  }
};

var shinyNewWidget = {
  html: "",
  init: function() {
    this.html = document.createElement("section");
  }
};

// Initialize with different HTML elements
shinyNewWidget.init();

// Run the original method with the new context object
legacyWidget.showModal.call(shinyNewWidget, "p");

If you're still confused about explicit binding, think of it as a basic template for reusing code. This may seem tedious, but it's a good way to refactor legacy JS code.

In addition, you may want to know what is apply and bind. Apply has the same effect as call, except that the former accepts an array of parameters and the latter is a list of parameters.

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams.call(newObj, "aa", "bb", "cc");

And apply needs an array of parameters.

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams.apply(newObj, ["aa", "bb", "cc"]);

What about bind? Bind is the most powerful way to bind functions. Bind still accepts a new context object for the given function, but it does not just call the function with the new context object, but returns a new function that is permanently bound to the object.

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

var newFunc = obj.printParams.bind(newObj);

newFunc("aa", "bb", "cc");

A common use case of bind is to rebind this of the original function permanently:

var obj = {
  version: "0.0.1",
  printParams: function(param1, param2, param3) {
    console.log(this.version, param1, param2, param3);
  }
};

var newObj = {
  version: "0.0.2"
};

obj.printParams = obj.printParams.bind(newObj);

obj.printParams("aa", "bb", "cc");

From now on, this in obj.printParams always points to newObj. Now it should be clear why bind is used in React to rebind class methods.

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(() => {
      return { text: "PROCEED TO CHECKOUT" };
    });
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.text || this.props.text}
      </button>
    );
  }
}

But the reality is more subtle, with "missing bindings." When we assign the event handler to the React element as a prop, the method will be passed as a reference instead of a function, just like passing the event handler reference in another callback:

// Loss binding
const handleClick = this.handleClick;

element.addEventListener("click", function() {
  handleClick();
});

Assignment breaks the binding. In the example component above, the handleClick method (assigned to the button element) attempts to update the state of the component by calling this.setState(). When this method is called, it has lost its binding and is no longer the class itself: its context object is now the window global object. At this point, you get an error of "TypeError: Cannot read property 'setState' of undefined".

Most of the React components are exported as ES2015 modules: this is undefined, because the ES module uses strict mode by default, so the default binding is disabled, and the class of ES6 also enables strict mode. We can use a simple class to simulate the React component for testing. handleClick calls the setState method in response to the click event

class ExampleComponent {
  constructor() {
    this.state = { text: "" };
  }

  handleClick() {
    this.setState({ text: "New text" });
    alert(`New state is ${this.state.text}`);
  }

  setState(newState) {
    this.state = newState;
  }

  render() {
    const element = document.createElement("button");
    document.body.appendChild(element);
    const text = document.createTextNode("Click me");
    element.appendChild(text);

    const handleClick = this.handleClick;

    element.addEventListener("click", function() {
      handleClick();
    });
  }
}

const component = new ExampleComponent();
component.render();

The wrong line of code is

const handleClick = this.handleClick;

Then click the button to view the console, and you will see "typeerror: cannot read property 'setstate' of defined"... To solve this problem, bind can be used to bind methods to the correct context, that is, the class itself

  constructor() {
    this.state = { text: "" };
    this.handleClick = this.handleClick.bind(this);
  }

Click the button again to run correctly. Explicit binding is stronger than implicit binding and default binding. With apply, call, and bind, we can modify a function at will by providing it with a dynamic context object.

Rule 4: 'new' binding

Constructor mode, which helps to encapsulate the behavior of creating new objects with JS:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"

Here, let's create a blueprint for an entity called "Person." According to this blueprint, you can call "construct" a new object of type Person through "new":

var me = new Person("Valentino");

There are many ways to change this point in JS, but when you use new on a constructor, this point is determined, and it always points to the newly created object. Any function defined on the constructor prototype, as follows

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

This always knows what "this" points to, because most of the time this points to the host object of the operation. In the following example, greet is called by me

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"

Since me is constructed by constructor calls, its meaning is not ambiguous. Of course, you can still borrow a greet from Person and run it with another object:

Person.prototype.greet.apply({ name: "Tom" });

// Output: "Hello Tom"

As we can see, this is very flexible, but if we don't know the rules on which this is based, we can't make informed guesses and make use of its real power. To make a long story short, this is based on four "simple" rules.

Arrow function and "this"

The syntax of arrow functions is convenient and concise, but it is recommended not to abuse them. Of course, arrow functions have many interesting features. Consider a constructor called Post first. As long as we create a new object from the constructor, there will be a Fetch request for the REST API:

"use strict";

function Post(id) {
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(function(response) {
      return response.json();
    })
    .then(function(json) {
      this.data = json;
    });
}

var post1 = new Post(3);

The above code is in strict mode, so the default binding (back to global this) is forbidden. Attempting to run the code in the browser will result in an error: "typeerror: cannot set property 'data' of undefined at: 11:17".

It's right to report a mistake. The global variable this is undefined in strict mode. Why does our function try to update window.data instead of post.data?

The reason is simple: the callback triggered by Fetch runs in the browser, so it points to window. In order to solve this problem, there was an old practice in the early days, which was to use temporary as well as "that". In other words, save this reference in a variable named that:

"use strict";

function Post(id) {
  var that = this;
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(function(response) {
      return response.json();
    })
    .then(function(json) {
      that.data = json;
    });
}

var post1 = new Post(3);

If this is not the case, the easiest way is to use the arrow function:

"use strict";

function Post(id) {
  this.data = [];

  fetch("https://jsonplaceholder.typicode.com/posts/" + id)
    .then(response => {
      return response.json();
    })
    .then(json => {
      this.data = json;
    });
}

var post1 = new Post(3);

Problem solving. Now this.data always points to post1. Why? The arrow function points this to its closed environment (also known as "lexical scope"). In other words, the arrow function doesn't care if it's running in a window object. Its enclosing environment is the object post1, which is hosted by post1. Of course, this is one of the most interesting use cases of the arrow function.

summary

What is this in JS? It depends. This is based on four rules: default binding, implicit binding, explicit binding and "new" binding.

Implicit binding means that when a function references this and runs as part of a JS object, this will point to the "host" object. But JS functions always run in an object, as any global function is defined in a so-called global scope.

When working in a browser, the global scope is window. In this case, any function running in the global will see that this is window: it is the default binding for this.

In most cases, you don't want to interact with the global scope, so JS provides a way to neutralize the default binding in strict mode. In strict mode, any reference to the global object is undefined, which effectively protects us from stupid mistakes.

In addition to implicit binding and default binding, there are also "explicit binding". We can use three methods to achieve this: apply, call and bind. These methods are useful for passing explicit host objects on which a given function should run.

Last but not least, the "new" binding, which does five things at the bottom by calling the "constructor." For most developers, this is a terrible thing to avoid at all costs. But for those who want to study deeply, this is a powerful and flexible system, which can reuse JS code.

The bugs that may exist after code deployment can't be known in real time. In order to solve these bugs afterwards, a lot of time has been spent on log debugging. Here is a good BUG monitoring tool recommended by the way. Fundebug.

Original text: https://github.com/valentinog...

Communication

Alibaba cloud has been doing activities recently, with a discount of 20%. If you are interested, please take a look at: https://promotion.aliyun.com/...

The articles of dry goods series are summarized as follows. I think it's a good idea to order a Star. Welcome to learn from each other.

https://github.com/qq449245884/xiaozhi

Because of the space limitation, today's sharing is only here. If you want to know more, you can scan the QR code at the bottom of each article, and then pay attention to our wechat public account to learn more information and valuable content.

I don't go to bed until 2 o'clock every time I sort out the articles. It's very painful. I hope I can support you and encourage you.

Posted by BahBah on Mon, 21 Oct 2019 17:10:31 -0700