[译]箭头函数的this arrow this

本文根据@Patreon上的《Arrow This》所译,如果译得不好或有不对之处还请多多指点。如需转载此译文,需注明英文出处:https://blog.getify.com/arrow-this/

ES6中值得称赞的特性之一就是提供函数表达式缩写定义的箭头函数语法(又名匿名函数)。你很难发现关于ES6(或者甚至甚少与其相关的)的一篇文章、会议演讲或者书都没有首要介绍=>是新的function

箭头函数语法已经贯穿整个标准和规范文档,就好像它一直在那里,我们只是正好发现它。

那些追随我的人知道我不是=>语法的爱好者,因为有许多原因。但是别担心,这篇文章不是关于为什么我不喜欢它。如果你对这个讨论感兴趣,可以参考我的书YDKJS:ES6 & Beyond的第二章Arrow Functions

围绕箭头函数和this, arguments等究竟做了什么,本篇文章将澄清一些疑惑。事实上,过去没有准确地解释这个话题我感觉到是一种罪过,这里我要清除记录。例如,一段时间后我在YDKJS第一次解释了它

词法 是还不是?

我和其他的很多人怎么描述箭头函数的this的行为:词法this

我们究竟指的是什么?

function foo() {
   setTimeout( () => {
      console.log("id:", this.id);
   },100);
}

foo.call( { id: 42 } );
// id: 42

这里的=>箭头函数似乎绑定了它的this到父函数foo()this上。

如果那个内部函数是一个普通的function(函数声明或者函数表达式),它的this本来是由setTimeout(...)选择调用函数时决定。如果你对决定this绑定的规则比较迷惑,看一下我的书YDKJS: this & Object Prototypes的第二章Determining this

词法变量 this

一个说明可看见的this行为的通用方法是:

function foo() {
   var self = this;
   setTimeout(function() {
      console.log("id:", self.id);
   },100);
}

foo.call( { id: 42 } );
// id: 42

边注:变量名self是一个绝对可怕的,有误导的名字。它意味着this是函数本身的引用。但它从来没有。var that = this语义上同样无益,尤其当有多个作用域生效的时候(that1,that2,…)。如果你想要一个恰当的名字,使用var context = this,因为那就是this真正的含义:一个动态的上下文

在上面这个片段中,你可以看到我们甚至没有在内部函数使用this。相反,我们回退到一个更可预见性的机制:词法变量。我们在外部函数声明一个变量self,然后简单的在内部函数引用它。

这种完全消除了this绑定规则(对于内部函数,也是)。取而代之的是仅仅依赖语法作用域规则,实际上是闭包。

最终的结果似乎和=>箭头函数一样,换句话说,这里的(不严谨的)含义是=>箭头函数有一个了类似语法变量/闭包机制的方式的“语法this”行为。

但是……那是不精确的
嗯哦。我的错。

箭头绑定this

另一个说明可看见的=>箭头函数this的行为的方式是认为内部函数被强制绑定:

function foo() {
   setTimeout(function() {
      console.log("id:", this.id);
   }.bind(this),100);
}

foo.call( { id: 42 } );
// id: 42

正如你看到的.bind(this),这里的内部函数被强制绑定到外部函数的this,也意味着不管setTimeout(...)如何调用函数,调用函数始终使用foo()函数使用的this

是的,这个版本有着和先前两个代码片段同样可观察到的行为。因此,它更精确吗?很多人认为那就是=>箭头函数实际上怎样运作的。
额……不。
哎呦。

先前语法

我感到更加尴尬对于过去我的含糊不清的解释和对其他的解释的容忍,因为一段时间后TC39的正式员工Dave Herman@littlecalculist向我仔细准确地解释了这个问题,然后我还是没有完全吸收他的解释的含义的内在,我感到有罪。
Dave告诉我,本质上,“‘词法this’短语是令人不安的,因为this一直是词法”
真的吗?唔……
他继续说道,“=>改变的不是把this词法化,而是一点也不绑定它里面的this。”
我没有完全理解他说的话。但是我现在知道了。
普通函数声明它们自己的this,使用前述的规则中的一个。=>箭头函数就没有this
等等……这怎么可能?我很确信在一个=>箭头函数内使用this。你当然可以,但是怎么回事?既然=>箭头函数没有它自己的this,当你使用this时,普通词法作用域规则是生效的,引用被解析为包含最近的外部作用域定义的this
考虑下面这个代码:

function foo() {
   return () => {
      return () => {
         return () => {
            console.log("id:", this.id);
         };
      };
   };
}

foo.call( { id: 42 } )()()();
// id: 42

在这个片段中,你认为有多少个this绑定?大多数可能认为是4个,每个函数一个。
准确无误的来说,只有一个,是在foo()函数中。
相继嵌套的=>箭头函数没有声明自己的this,因此this.id简单的沿着作用域链解析直到找到foo(),第一个肯定绑定this的函数。

另一个普通的词法变量的处理方式和这个完全相同。

换句话说,就像Dave说的,this本来是词法,而且始终是词法。=>不绑定this变量,因此作用域继续向上查找如同它一直那样做一样。

不仅仅是this

如果你含糊不清地解释=>this的行为,你最终认为“箭头函数仅仅是function的语法糖……”,这是非常危险的,它们显然不是,也不是var self = this或者.bind(this)的语法糖。

那些不准确的解释是为错误的原因得到正确的答案的典型例子。就像回到高中的代数课,当你在考试中得到正确答案的时候但是你的老师指出你使用了不正确的方式得到答案。你怎样得到答案才重要!

此外,准确的解释——=>不绑定它自己的this,允许词法作用域解析——同样解释了另外一个=>箭头函数重要的事实:他们不改变this在内部函数是如何生效的。

事实上,=>箭头函数不绑定thisargumentssuper(ES6)或者new.target(ES6)。

那就对了,上面所有的四种情况下(未来可能更多!),=>箭头函数不绑定那些变量,因此任何对它们的引用都会沿着作用域链解析到它们外部的作用域。
考虑下面这个代码:

function foo() {
   setTimeout( () => {
      console.log("args:", arguments);
   },100);
}

foo( 2, 4, 6, 8 );
// args: [2, 4, 6, 8]

你看见了吗?
在这个片段中,arguments没有被=>限定,因此它被解析到foo()函数的argumentssupernew.target也是同样的结果。

为什么this重要?

更新:在评论和社交媒体中,很多人质疑既然this不管是理论或是实践上表现一样,为什么它真的重要?

你知道=>箭头函数不能让它们的this绑定被bind(...)强制限定吗?

考虑下面这个代码:

function foo() {
   return () => {
      console.log("id:",this.id);
   };
}

var arrowfn = foo.call( { id: 42 } );

setTimeout( arrowfn.bind( { id: 100 } ), 100);
// id: 42

所以为什么.bind({...})没有生效得到id: 100的输出?
如果你对=>箭头函数的this有不准确的理解,你不得不假设可以这样解释:“this是不可改变的。”,这是错误的。

简单正确的答案是既然=>没有this,当然.bind(obj)没有什么可以操作!类似地,=>不能被new调用。既然没有thisnew没有东西可以绑定。

理解了=>实际上怎么样处理this是非常重要的。因为这是唯一的方式去准确解释=>箭头函数其他可观察到的行为。当你觉得奇怪时,待在黑暗里,然后摸索奇怪的解释——这是不成熟的JS编码方式。

总结

不要满足于舒适但不准确的答案,不要满足于通过错误的方式得到正确的答案。
它影响事情是如何工作的。它影响你使用的构思模型,因为你会用它分析、描述和调试其他的行为。如果你在一开始就偏离了轨道,你未来只能还在偏离的轨道上。
希望我那时更仔细地倾听了Dave。希望没有不准确地解释了=>this的行为。我认真对待关于JS我所想的,所写的和教授的都是正确的,未来我会更加仔细。

Tweet上分享这篇文章