【有趣JS连载】以更细的角度来看 JS 中的 this(第8章)

揭秘 "this"

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

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

Python 类也有一个跟 this 差不多的东西,叫做 self

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

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

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时,都会返回正确的名字:

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 看起来似乎指向实际的类实例?

这是不对的。咱们不要忘记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引入了“安全模式”:严格模式。严格模式是通过使用 “useStrict”启用。严格模式中的一个好处就是消除了默认绑定。在严格模式下,当试图从全局上下文中访问 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>	
    );	
  }	
}

现在 ReactHooks使得类几乎没有必要了,但是仍然有很多使用ES6类的“遗留”React组件。大多数初学者会问的一个问题是,为什么咱们要在React中通过bind` 方法重新绑定事件处理程序方法?

call、 apply、 bind 这三个方法都属于 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代码需要重构,这种方式是非常合适的。

此外,你可能想知道什么是 apply和 bind。 apply具有与 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);	
  }

再次单击该按钮,运行正确。显式绑定比隐式绑定和默认绑定都更强。使用 apply、 call和 bind,咱们可以通过为函数提供一个动态上下文对象来随意修改它。


规则 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,这有效地保护了我们避免愚蠢的错误。

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

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

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

原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/README.md

结尾彩蛋

欢迎关注前端之阶公众号,获取更多前端知识,加入前端大群,与知名互联网大佬做朋友,开启共同学习新篇章!

640?wx_fmt=other

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值