【JS 口袋书】第 8 章:以更细的角度来看 JS 中的 this

做者:valentinogagliardi
译者:前端小智
来源:github

阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。html

揭秘 "this"

JS 中的this关键字对于初学者来讲是一个谜,对于经验丰富的开发人员来讲则是一个永恒的难题。this 其实是一个移动的目标,在代码执行过程当中可能会发生变化,而没有任何明显的缘由。首先,看一下this关键字在其余编程语言中是什么样子的。
如下是 JS 中的一个 Person 类:前端

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

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

Python 类也有一个跟 this 差很少的东西,叫作selfgit

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

Python类中,self表示类的实例:即从类开始建立的新对象github

me = Person('Valentino')

PHP中也有相似的东西:编程

class Person {
    public $name; 

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

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

这里$this是类实例。再次使用JS类来建立两个新对象,能够看到每当我们调用object.name时,都会返回正确的名字:json

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

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

const me = new Person("前端小智");
console.log(me.name); // '前端小智'

const you = new Person("小智");
console.log(you.name); // '小智'

JS 中相似乎相似于PythonJavaPHP,由于 this 看起来彷佛指向实际的类实例?segmentfault

这是不对的。我们不要忘记JS不是一种面向对象的语言,并且它是宽松的、动态的,而且没有真正的类。this与类无关,我们能够先用一个简单的JS函数(试试浏览器)来证实这一点:数组

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

whoIsThis();

规则1:回到全局“this”(即默认绑定)

若是在浏览器中运行如下代码浏览器

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

whoIsThis();

输出以下:安全

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

如上所示,我们当 this 没有在任何类中的时候,this 仍然有值。当一个函数在全局环境中被调用时,该函数会将它的this指向全局对象,在我们的例子中是window

这是JS的第一条规则,叫做默认绑定。默认绑定就像一个回退,大多数状况下它是不受欢迎的。在全局环境中运行的任何函数均可能“污染”全局变量并破坏代码。考虑下面的代码:

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

第一个开发人员建立一个名为globalSum的全局变量,并为其分配一个函数。接着,另外一个开发人员将null分配给相同的变量,从而致使代码出现故障。

处理全局变量老是有风险的,所以JS引入了“安全模式”:严格模式。严格模式是经过使用“use Strict”启用。严格模式中的一个好处就是消除了默认绑定。在严格模式下,当试图从全局上下文中访问this时,会获得 undefined

"use strict";

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

whoIsThis();

// Output: undefined

严格的模式使JS代码更安全。

小结一下,默认绑定是JS中的第一条规则:当引擎没法找出this是什么时,它会返回到全局对象。接下看看另外三条规则。

规则2: 当“this”是宿主对象时(即隐式绑定)

“隐式绑定”是一个使人生畏的术语,但它背后的理论并不那么复杂。它把范围缩小到对象。

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

当一个函数被赋值为一个对象的属性时,该对象就成为函数运行的宿主。换句话说,函数中的this将自动指向该对象。这是JS中的第二条规则,名为隐式绑定。即便在全局上下文中调用函数,隐式绑定也在起做用

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

whoIsThis();

我们没法从代码中看出,可是JS引擎将该函数分配给全局对象 window 上的一个新属性,以下所示:

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

我们能够很容易地证明这个假设。在浏览器中运行如下代码:

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

console.log(typeof window.whoIsThis)

打印"function"。对于这一点你可能会问:在全局函数中this 的真正规则是什么?

像是缺省绑定,但实际上更像是隐式绑定。有点使人困惑,但只要记住,JS引擎在在没法肯定上下文(默认绑定)时老是返回全局this。另外一方面,当函数做为对象的一部分调用时,this 指向该调用的对象(隐式绑定)。

规则 3: 显示指定 “this”(即显式绑定)

若是不是 JS 使用者,很难看到这样的代码:

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

这就是显式绑定,在 React 会常常看到这中绑定方式:

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>
    );
  }
}

如今React Hooks 使得类几乎没有必要了,可是仍然有不少使用ES6类的“遗留”React组件。大多数初学者会问的一个问题是,为何我们要在 React 中经过 bind` 方法从新绑定事件处理程序方法?

callapplybind 这三个方法都属于Function.prototype。用于的显式绑定(规则3):显式绑定指显示地将this绑定到一个上下文。但为何要显式绑定或从新绑定函数呢?考虑一些遗留的JS代码:

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是绑定到对象legacyWidget的“方法”。this.html 属于硬编码,把建立的元素写死了(div)。这样我们没有办法把内容附加到我们想附加的标签上。

解决方法就是可使用显式绑定this来更改showModal的对象。。如今,我们能够建立一个小部件,并提供一个不一样的HTML元素做附加的对象:

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");
  }
};

接着,使用 call 调用原始的方法:

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");
  }
};

// 使用不一样的HTML元素初始化
shinyNewWidget.init();

// 使用新的上下文对象运行原始方法
legacyWidget.showModal.call(shinyNewWidget, "p");

若是你仍然对显式绑定感到困惑,请将其视为重用代码的基本模板。这种看起来有点繁琐冗长,但若是有遗留的JS代码须要重构,这种方式是很是合适的。

此外,你可能想知道什么是applybindapply具备与call相同的效果,只是前者接受一个参数数组,然后者是参数列表。

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");

apply须要一个参数数组

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"]);

那么bind呢? bind 是绑定函数最强大的方法。bind仍然为给定的函数接受一个新的上下文对象,但它不仅是用新的上下文对象调用函数,而是返回一个永久绑定到该对象的新函数。

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");

bind的一个常见用例是对原始函数的 this 永久从新绑定:

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");

从如今起obj.printParams 里面的 this 老是指向newObj。如今应该清楚为何要在 React 使用 bind来从新绑定类方法了吧。

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>
    );
  }
}

但现实更为微妙,与“丢失绑定”有关。当我们将事件处理程序做为一个prop分配给React元素时,该方法将做为引用而不是函数传递,这就像在另外一个回调中传递事件处理程序引用:

// 丢失绑定
const handleClick = this.handleClick;

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

赋值操做会破坏了绑定。在上面的示例组件中,handleClick方法(分配给button元素)试图经过调用this.setState()更新组件的状态。当调用该方法时,它已经失去了绑定,再也不是类自己:如今它的上下文对象是window全局对象。此时,会获得"TypeError: Cannot read property 'setState' of undefined"的错误。

React组件大多数时候导出为ES2015模块:this未定义的,由于ES模块默认使用严格模式,所以禁用默认绑定,ES6 的类也启用严格模式。我们可使用一个模拟React组件的简单类进行测试。handleClick调用setState方法来响应单击事件

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();

错误的代码行是

const handleClick = this.handleClick;

而后点击按钮,查看控制台,会看到 ·"TypeError: Cannot read property 'setState' of undefined"·.。要解决这个问题,可使用bind使方法绑定到正确的上下文,即类自己

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

再次单击该按钮,运行正确。显式绑定比隐式绑定和默认绑定都更强。使用applycallbind,我们能够经过为函数提供一个动态上下文对象来随意修改它。

规则 4:"new" 绑定

构造函数模式,有助于用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"

这里,我们为一个名为“Person”的实体建立一个蓝图。根据这个蓝图,就能够经过“new”调用“构造”Person类型的新对象:

var me = new Person("Valentino");

在JS中有不少方法能够改变 this 指向,可是当在构造函数上使用new时,this 指向就肯定了,它老是指向新建立的对象。在构造函数原型上定义的任何函数,以下所示

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

这样始终知道“this”指向是啥,由于大多数时候this指向操做的宿主对象。在下面的例子中,greet是由me的调用

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

// Output: "Hello Valentino"

因为me是经过构造函数调用构造的,因此它的含义并不含糊。固然,仍然能够从Person借用greet并用另外一个对象运行它:

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

// Output: "Hello Tom"

正如我们所看到的,this很是灵活,可是若是不知道this所依据的规则,我们就不能作出有根据的猜想,也不能利用它的真正威力。长话短说,this是基于四个“简单”的规则。

箭头函数和 "this"

箭头函数的语法方便简洁,可是建议不要滥用它们。固然,箭头函数有不少有趣的特性。首先考虑一个名为Post的构造函数。只要我们从构造函数中建立一个新对象,就会有一个针对REST API的Fetch请求:

"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);

上面的代码处于严格模式,所以禁止默认绑定(回到全局this)。尝试在浏览器中运行该代码,会报错:"TypeError: Cannot set property 'data' of undefined at :11:17"

这报错作是对的。全局变量 this 在严格模式下是undefined为何我们的函数试图更新 window.data而不是post.data?

缘由很简单:由Fetch触发的回调在浏览器中运行,所以它指向 window。为了解决这个问题,早期有个老作法,就是使用临时亦是:“that”。换句话说,就是将this引用保存在一个名为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);

若是不用这样,最简单的作法就是使用箭头函数:

"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);

问题解决。如今 this.data 老是指向post1。为何? 箭头函数将this指向其封闭的环境(也称“词法做用域”)。换句话说,箭头函数并不关心它是否在window对象中运行。它的封闭环境是对象post1,以post1为宿主。固然,这也是箭头函数最有趣的用例之一。

总结

JS 中 this 是什么? 这得视状况而定。this 创建在四个规则上:默认绑定、隐式绑定、显式绑定和 “new”绑定。

隐式绑定表示当一个函数引用 this 并做为 JS 对象的一部分运行时,this 将指向这个“宿主”对象。但 JS 函数老是在一个对象中运行,这是任何全局函数在所谓的全局做用域中定义的状况。

在浏览器中工做时,全局做用域是 window。在这种状况下,在全局中运行的任何函数都将看到this 就是 window:它是 this 的默认绑定。

大多数状况下,不但愿与全局做用域交互,JS 为此就提供了一种用严格模式来中和默认绑定的方法。在严格模式下,对全局对象的任何引用都是 undefined,这有效地保护了咱们避免愚蠢的错误。

除了隐式绑定和默认绑定以外,还有“显式绑定”,咱们可使用三种方法来实现这一点:applycallbind。 这些方法对于传递给定函数应在其上运行的显式宿主对象颇有用。

最后一样重要的是“new”绑定,它在经过调用“构造函数”时在底层作了五处理。对于大多数开发人员来讲,this 是一件可怕的事情,必须不惜一切代价避免。可是对于那些想深刻研究的人来讲,this 是一个强大而灵活的系统,能够重用 JS 代码。

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

原文:https://github.com/valentinog...

交流

阿里云最近在作活动,低至2折,有兴趣能够看看:https://promotion.aliyun.com/...

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

每次整理文章,通常都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励

clipboard.png