Arrow This 理清箭头函数

原文 Arrow This: Clearing Up Confusion over Arrow Functions

Arrow This 理清箭头函数

ES6中一个最值得称赞的功能是新的箭头函数=>即函数表达式(拉姆达)的简写。你很难发现哪个关于ES6的博客文章或者会议讨论或者书籍会不讨论=> 是新的功能。

箭头函数语法甚至充斥在标准和规范文档中,好像它一直在那里而我们只是刚发现它

关注我的人知道我并不是箭头函数的粉丝,由于一系列原因,不过别担心,本文并不是讨论我为什么不喜欢它,如果你对这个话题感兴趣,请看第二章:箭头函数 来自我的书 你不了解的JS:ES6 & Beyond

这里我要理清一些混乱关于箭头函数中的 this 和 argument 等等.其实,我已经快要讲不清楚这个话题,我想清楚记录。例如,这里有我初次尝试解释它 在 你不知道的JS系列中.

关键字与否? (Lexical Or Not?)

这是我和很多的人,描述箭头函数中this的行为:关键字 this

    function foo(){
        setTimeout(()=> {
            console.log('id:', this.id)
        },100);
    }
    
    foo.call({id:42});
    //id:42

箭头函数在这里将它的this绑定到父函数foo()的this上。如果内部函数有一个常见的function(声明或者表达式),它的this将会受到setTimeout的控制。如果你对this-绑定的规则感到困惑,请查看第二章:判断this在我的书你不知道的JS:this&对象原型

关键字声明 this (Lexical Variable this)

一个常见的观察this的行为的方式是

    function foo(){
        var self = this; 
        setTimeout(()=> {
            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-绑定的影响规则,代替为依靠语法作用空间的规则,或者更进一步:闭包。

结果显示了和=>箭头函数一样的结果,换句话说,(不精准)这里的含义是=>箭头函数中语法变量/闭包行为 与 ‘lexical this’行为一致。 但是,这并不精准。

###箭头绑定 this (Arrow-bound 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.

是的,这个版本与前两个片段的行为一致。所以这说明神门?大多数人假设这就是=>箭头函数的真实行为。

恩…并不正确

###Already Lexical 我对我不精准的解释感到尴尬,和他人的宽容解释它。在过去的一段时间里,TC39制定者 Dave Herman 向我小心并准确的解释了它,但我怀疑我并没有完整额消化他的解释的含义。

Dave对我说,实质上,关键字(lexical)this或许是麻烦的,因为this已经是关键字(lexical)

他继续说“=>改变的不是创建this关键字,更多的是不绑定this给它(‘not bind a this in it at all’)”

我当时没有跟上他的思路,但是我现在明白了这一点。

普通的函数声明了他自己的this,就像前边的规则,=>箭头函数实际上并没有this

等等…这怎么可能,我完全可以在一个箭头函数内使用this啊。你当然可以使用,既然箭头函数没有自己的this,当你使用this时,普通的词法作用域起了作用,即引用了离他最近的作用域定义this.

考虑以下代码

    function foo(){
        return ()=>{
            return ()=>{
                return ()=>{
                    console.log('id:',this.id)
                };
            };
        };
    }
    
    foo.call({id:42})()()();
    //id:42

在这个代码片段中,你觉得有多少this-绑定,大概有四个,每个函数一个。

更准确的说:只有一个,在foo()函数内

依次嵌套的=>函数没有声明自己的this,所以this.id引用了上层作用域直到获取到即foo(),第一个明确绑定了this的位置。

这也是普通其他语法变量使用的方式!

换句话说,就像Dave说的,this 是一个关键字 ,总是一个关键字 。=>箭头函数不绑定this变量,所以作用域会继续搜寻它就像对待普通的做法。

###不只是this (Not Just this)

如果你不准确的解释this在=>箭头函数中的行为时一个潜在的风险是你最终会这么想,‘箭头函数只是function的语法糖…’,他们显然不是,就不是var self=this或者bind(this)的语法糖。

这些不精准的解释是一个经典例子关于’获得错误原因的准确答案’。就像高中代数课上,你解出正确答案老师却给你判了错误因为你使用了错误的技术。你怎么得到问题答案。

更进一步,准确的解释–=>不绑定自己的this,允许语法作用域解决方案覆盖–同样解释了=>箭头函数中的其他重要因素:它们不仅改变了this在内部函数中的行为.事实上,=>箭头函数不绑定this,arguments,super(ES6),以及new.target(ES6)

在这全部四个因素中,=>箭头函数不绑定他们的局部变量,所以任何引用都会改变他们作用域到另一个外部作用域

考虑:

    function foo(){
       setTimeout( () => {
        console.log('arguments:',arguments)
       },100)
    }
    
    foo2468
    //arguments:[2,4,6,8]

你看到了吗?

在这个片段中,arguments不绑定到=>,所以它使用foo()的arguments代替。super和new.target也会有同样的行为。

为何 this 有问题?

update: 在社区中,很多人问为何有这种事情,关于this的行为却忽视理论与现实。

你知道=>箭头函数不能通过bind来硬绑定他们的this吗?

    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调用,因为没有this,所以new不能绑定任何东西。

Written on October 30, 2015