ES6 箭頭函數中的 this?你可能想多了(翻譯)


箭頭函數=>無疑是ES6中最受關注的一個新特性了,通過它可以簡寫 function 函數表達式,你也可以在各種提及箭頭函數的地方看到這樣的觀點——“=> 就是一個新的 function”。

箭頭函數的句法規則甚至早已延伸到各項標准和技術文檔中去了,雖然它早已不稀奇,卻給我們一種剛剛發現的新鮮感。 

粉我的人都知道俺因為某些原因不怎么喜歡 => 的語法,不過別擔心,本文並非講述我為何不喜歡它,如果你對這個觀點感興趣,可以查看我《YDKJS:ES6 & Beyonf》一書的第二章

我想在這里理清一下箭頭函數到底對 this 和 arguments 等東東做了些啥,事實上我在之前從未准確解釋過這一點,對此感到有點愧疚,於是乎想洗白下自己。你可以在這里看到我對於該話題的第一次陳述。

是否局部(Lexical)?

包括我在內的許多人,都會這么描述箭頭函數里 this 的行為:局部的 this。

什么意思呢?

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

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

這里的 => 箭頭函數看起來把它內部的 this 綁定為父函數 foo() 里的 this。如果這個內部函數是一個常規的函數(聲明或表達式),它的 this 將類似 setTimeout 如何調用函數一樣被控制着。如果你對 this 綁定的規則還不清楚,可以查閱我《YDKJS:this & Object Prototypes》一書的第二章

局部變量 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 曾更仔細、准確地向我闡述過這個問題,但我很愧疚一直沒能完全了解他所陳述的含義,因此對於我往日不准確的言論我就更感歉意了,也更能接納他人的觀點。

Dave 主要對我這么說,“你提及的'局部 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

如果你貿貿然地同意了“箭頭函數就是常規function的語法糖”這樣的觀點,那是不正確的,因為事實並非如此——箭頭函數里並不按常規支持 var self = this 或者 .bind(this) 這樣的糖果。

那些錯誤的解釋都是典型的“給對了答案卻講錯了原因”,就像你在高中代數課的測試上明明寫對了答案,但老師仍會畫圈圈告訴你用錯方法了——如何解得答案才是最重要的!

另外,關於“=>箭頭函數不綁定自身的 this,而允許局部作用域的方案來沿襲處理之”的正確描述,也解釋了箭頭函數的另一個情況——它們在函數內部不走尋常路的孩子不僅僅是 this。

事實上 =>箭頭函數並不綁定 this,arguments,super(ES6),抑或 new.target(ES6)。

這是真的,對於上述的四個(未來可能有更多)地方,箭頭函數不會綁定那些局部變量,所有涉及它們的引用,都會沿襲向上查找外層作用域鏈的方案來處理。

思考下這段代碼:

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

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

這段代碼中,=>箭頭函數並沒有綁定 arguments,所以它會以 foo() 的 arguments 來取而代之,而 super 和 new.target 也是一樣的情況。

總結

不要不經思考就輕易接受那些不准確的答案,不用滿足於那些通過錯誤形式獲取到的正確答案。

這關系到了事物是怎樣作業的,以及你使用了怎樣的心智模型(mental model),你會使用這種心智模型去分析、描述和調試其它的行為,如果你在一開始的時候就偏離了軌道,那么在之后你也只會一直停留在錯誤的軌道上。

我后悔當初沒有更仔細地聆聽 Dave 的觀點,也好希望當初自己木有發表過關於=>箭頭函數的錯誤言論。我會在今后思考、提筆、傳道JS的時候更加嚴格地確保其正確性,也會讓自己更加小心謹慎。

 

原文 http://blog.getify.com/arrow-this/

donate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM