Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
husky-dot opened this issue Oct 23, 2019 · 1 comment
Open

Comments

@husky-dot
Copy link
Owner

作者:valentinogagliardi

译者:前端小智

来源:github


阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r


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

揭秘 "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引入了**“安全模式”:严格模式。严格模式是通过使用“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/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/README.md

交流

阿里云最近在做活动,低至2折,有兴趣可以看看:https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r

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

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

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

@yihong0618
Copy link

谢谢作者。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants