本文为译文,原文为 A different way of understanding this in JavaScript
原文地址:http://2ality.com/2017/12/alternate-this.html
理解javaScript中this的另一种方法
在这篇博文中,我将通过不同以往的方法来解释javaScript中的this:假设箭头函数是真正的函数,而普通函数是方法的特殊构造。我认为这样会使得this更加容易理解---来试一试吧
1. 两种类型的函数
在这篇博文里,我们关注两种不同的函数:
普通函数:function () { }
箭头函数: ()=> { }
1.1 普通函数
通过以下的方式创建一个普通的函数:
function add(x, y) { return x + y; }
每个普通函数在被调用时都填充了一个隐式参数this,换句话说,下面的两个表达式是等价的(在严格模式下):
add(3,5); add(underfined, 3, 5);
如果你嵌套不同函数,this是隐式的
function outer(){function inner() {console.log(this); // underfined}console.log(this); //outer;inner(); } outer.call(‘outer‘);
内部的inner(),this并没有指向outer的this,因为inner()拥有自己的this。
如果this是一个显示参数,这段代码将变成如下所示:
function outer(_this){function inner(_this) {console.log(_this); // underfined}console.log(_this); //outer;inner(underfined); } outer.call(‘outer);
请注意inner()对outer中的this的影响就像变量在嵌套作用域中所启到的作用一样:
const _this = outer; console.log(_this); //‘outer’{const _this = undefined; console.log(_this); // undefined}
由于普通函数总是存在着一个隐式参数this,因此‘方法’这个名字应该更适合它们。
1.2箭头函数
如下创建一个箭头函数(我使用了一个代码块以至于它更像一个函数声明)
const add = (x, y){const inner = () = > {return x+y; } }
如果你在一个普通函数里内部嵌套一个箭头函数,this 是不会受到影响的
function outer(){const inner = () => {console.log(this); // outer}console.log(this); //outer;inner(); } outer.call(‘outer);
由于箭头函数的这种表现,我偶尔也把它称之为‘真正的函数’。
比起普通函数,箭头函数与大多数的编程语言中的函数更加相似。
请注意箭头函数的中的this不会因为使用了.call()而受影响的,或者换句话说,它在被创建的时候就已经被固定在箭头函数所包含的作用域里了,例如:
function ordinary() {const arrow = ()=> this;console.log(arrow.call(‘goodbye’)); //hello} Ordinary.call(‘hello’);
1.3做为方法的普通函数
如果一个普通函数是一个对象的属性值,那么这个普通函数就变成了一个方法。
const obj = {prop: function () {} }
访问一个对象的属性的方法之一是通过.点操作符,这个操作符有两种不同模式:
获取和设置属性:obj.prop
调用方法:obj.prop(x, y)
第二个等价于:
obj.prop.call(this, x, y)
可以看出,当普通函数被调用时this再一次地被填充进去了。
JavaScript里有特殊的,更便利的语法来定义方法:
const obj = { prop() {} }
2. 公共的缺陷
让我们透过刚刚所学的知识来看一看一些公共的缺陷。
2.1缺陷:访问回调中的this(Promises)
一旦异步函数cleanupAsync()执行完毕,请思考以下基于Promise的代码,在该代码中我们打印了“Done”。
// Inside a class or an object literal:PerformCleanup() { CleanupAsync().then(function (){ This.logStatus(‘Done’); // (A)}) }
问题在于this.LogStatus()在执行到A行时就报错了,因为这时的this没有指向我们所预想的那个this。PerformCleanup()this受到了回调函数中的this的影响。换句话说:当我们想要使用一个箭头函数的时候使用了一个普通函数,如果我们改成下面的写法,那问题就解决了:
// Inside a class or an object literal:PerformCleanup() { CleanupAsync().then( () => { This.logStatus(‘Done’); }) }
2.2缺陷:访问回调中的this(.map)
类似的,下面的代码也将会在A行中失效,因为回调又影响到了prefixName方法中的this。
// Inside a class or an object literal:prefixNames(names) { return names.map(function (name) { return this.company + ': ' + name; // (A) }); }
我们再一次使用箭头函数来修改一下:
// Inside a class or an object literal:prefixNames(names) { return names.map( name => this.company + ': ' + name); }
2.3缺陷:使用方法作为回调
下面是一个UI组件的类
class UiComponent { constructor(name) { this.name = name; const button = document.getElementById('myButton'); button.addEventListener('click', this.handleClick); // (A) } handleClick() { console.log('Clicked '+this.name); // (B) } }
在A行中,UiComponent 注册了一个click事件处理程序。如果这个处理程序被触发,你会得到一个错误提示:
TypeError: Cannot read property 'name' of undefined
为什么呢?在A行,我们使用了一个正常的点操作符,而不是一个特殊的方法来调用这个点操作符,因此,存储在handleClick中的函数变成了处理程序,也就是说,大致发生了以下这些事情:
const handler = this.handleClick; handler(); //相当于: handler.call(undefined);
结果,this.name就在B行处报错了。
那我们该怎么修正它呢?问题在于,调用方法的点运算符不仅仅是先读取属性然后调用结果的组合。它做得更多。因此,当我们抽出一个方法时,我们需要自己提供缺失的部分,并通过函数方法.bind()(line A)为this填充一个固定值:
class UiComponent { constructor(name) { this.name = name; const button = document.getElementById('myButton'); button.addEventListener( 'click', this.handleClick.bind(this)); // (A) } handleClick() { console.log('Clicked '+this.name); } }
现在,this是固定的了,并且不会因为使用了正常的函数来调用而受到影响了。
function returnThis() { return this; }const bound = returnThis.bind('hello'); bound(); // 'hello'bound.call(undefined); // 'hello'
3. 保持安全的规则
避免问题的最简单方法是避免使用普通函数,并且始终使用方法定义或箭头函数。
然而,我更倾向于函数声明的语法。函数提升在有些时候也是有用的。如果你不在里面引用this,你可以安全地使用它们。这里有一个ESLint规则,可以帮助你。
31.不要把this当做参数来使用
一些API通过this来提供参数信息。我不喜欢那样,因为它阻止了你使用箭头函数,并违背了最初提到的简单经验法则。
来看下面的一个例子:beforeEach()函数将API对象通过this传递给它的回调函数。
beforeEach(function () { this.addMatchers({ // access API object toBeInRange: function (start, end) { ··· } }); });
重写这个函数很容易:
beforeEach(api => { api.addMatchers({ toBeInRange(start, end) { ··· } }); });
4. 进一步阅读
博文 “JavaScript’s this: how it works, where it can trip you up” [深入理解 this]
文章 [探索ES6]系列中的 “Callable entities in ECMAScript 6”
作者:生物股长_pld
链接:https://www.jianshu.com/p/40238ef1b721