共计 9980 个字符,预计需要花费 25 分钟才能阅读完成。
作者:valentinogagliardi
译者:前端小智
来源:github
阿里云最近在做活动,低至 2 折,有兴趣可以看看:
https://promotion.aliyun.com/…
为了保证的可读性,本文采用意译而非直译。
揭秘 “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 中类似乎类似于 Python、Java 和PHP,因为 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` 方法重新绑定事件处理程序方法?
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/valentinog…
交流
阿里云最近在做活动,低至 2 折,有兴趣可以看看:https://promotion.aliyun.com/…
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,一般都到 2 点才睡觉,一周 4 次左右,挺苦的,还望支持,给点鼓励