/ JavaScript

Arrow This

Original post: Arrow This by getify, this is a translated version in Traditional Chinese.


ES6 中大肆吹噓的一項功能是 => 箭頭函式語句(arrow function syntax),簡短的函式定義表達式(亦稱 "lambdas")。你很難找到一篇部落格文章、研討會演說或書籍有關 ES6(或無關)卻不說到 『=> 是新的 function』。

箭頭函式語句甚至滲透到標準及規格文件,幾乎像是已經存在許久而我們才剛剛發現。

關注我的人知道我不是 => 語句的粉絲,因為幾個原因。但別擔心,這篇文章不是說明為何我不喜歡。如果你對這個討論興趣,請見我的著作 YDKJS: ES6 & Beyond 書中的第 2 章,「箭頭函式」

這裡我希望澄清一些小小的困惑,關於箭頭函式與 thisarguments 等關係。事實上,過去沒有精確的解釋這個主題讓我一直感到內疚,我想要清楚地紀錄。例如,在 YDKJS 中我第一次解釋

Lexical Or Not?

我跟許多人描述箭頭函式 this 的行為是:lexical this

到底是什麼意思?

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

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

這裡 => 箭頭函式看上去像是繫結(bind)它的 this 到父函式 foo()this。如果內嵌(inner)函式是個普通函式(宣告式或表達式),它的 this 將受到 setTimeout(..) 如何選擇調用函式所控制。如果你對決定 this 繫結的規則感到不清楚,請見我的書籍 YDKJS: this & Object Prototypes 內的第 2 章,「決定 this

Lexical Variable 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 同樣地對語義十分無益,特別是當有多個生存空間(scopes)同時存在(that1that2、...)。如果要取個好名字,請用 var context = this,因為就是 this 真實意義:動態環境(context)

上述的程式片段中,可以看到內嵌函式沒有使用 this。取而代之的是,退回到一個較容易預測的機制:lexical variables。我們在外部函式宣告一個函數 self,接著單純的在內嵌函式參考這個變數。

當然這完全排除 this-繫結規則(對內嵌函式來說),取而代之的是依靠 lexical scoping 規則與 closure。

結果看上去與 => 箭頭函式相同。換句話說,這個(不精確)含意是 => 箭頭函式有 "lexical this" 行為,用的是 lexical variable/closure 機制的方法。

但是... 這並不精確

喔歐。我的錯。

箭頭-繫結 this

另一種描述觀察 => 箭頭函式 this 行為的方式,是將內嵌函式想成是強制-繫結(hard-bound):

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

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

你可以看到 .bind(this),內嵌函式已經強制-繫結到外部函式的 this,換句話說,無論 setTimeout(..) 如何選擇調用函式,該函式只會使用 foo() 函式的 this

是的,這個版本與前述兩個片段示範有相同觀察行為。所以,有比較精確嗎?許多人假定這就是 => 箭頭函式的運作模式。

呃...

Oops。

已經是 Lexical

我不精確的解釋及對於其他解釋的容許,讓我感到更加不堪,過去些陣子,TC39 成員 Dave Herman(@littlecalculist)曾經小心且精確地跟我說明這個問題,我沒有完全將他的說明完全內化感到十分內疚。

Dave 跟我說,本質上,『‘lexical this‘ 這個語句是個麻煩,因為 this 一直是 lexical』。

真的嗎?Hmm...

他接著說,『=> 改變的不是讓 this 變成 lexical,**而是不繫結 this **。』

過去我並沒有完全理解他說的。現在我瞭解了。

普通函式宣告他們的 this,使用的是前述的一個規則。=> 箭頭函式完全沒有 this

等等... 這怎麼可能?我很確定在 => 箭頭函式裡面可以使用 this。當然你可以。但是如何使用?由於 => 箭頭函式並沒有它自己的 this,當你使用 this,就會套用一般的 lexical scoping 規則,參考將會解析到最近的外部生存空間定義的 this

考慮:

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

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

上述片段中,一共有幾個 this-繫結?大多數會假設四個,每一個函式都有一個。

更精確地說,只有一個,就在 foo() 函式。

接續地巢狀 => 函式並未宣告它們的 this,所以 this.id 參考僅僅往上解析 scope 鍊一直到 foo(),這個地方是第一個找到肯定有 this 繫結的地方。

這跟與其他一般 lexical variable 是同樣的處理。

換句話說,如同 Dave 所說,this 已經是 lexical 且永遠是 lexical=> 並不會繫結 this 變數,所以 會持續查找 scope,如同一般所做。

不只是 this

如果你不精確地解釋 => 對於 this 的行為,最後就會想像成 "箭頭函式只是個 function... 的方便語句",很顯然不是。也不是 var self = this.bind(this) 的方便語句。

這些不精確的解釋是個經典的錯誤的理由正確的答案示範。回頭看看高中的代數課程,當你在測驗中提出正確解答,但是老師對你扣分,因為你用了錯誤的技巧取得答案。如何取得答案很重要!

此外,精確的解釋 — => 並不會繫結自己的 this,讓 lexical scope 分析來處理 — 也說明了另一個重要的事實:它並不會改變內嵌函式的 this 行為。

事實上,=> 箭頭函式不會繫結 thisargumentssuper(ES6)或 new.target(ES6)。

沒錯,這四種案例(未來可能還會有更多),=> 箭頭函式不會區域地繫結這些變數,所以任何參考到他們的地方將會 lexically 解析外部存在空間的 scope 鍊。

考慮:

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

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

看到沒?

這個片段中,arguments 被沒有被 => 做繫結,所以它解析到 foo(..)arguments。同樣地在 supernew.target 有一樣的結果。

為何 this 如此重要?

更新:在留言與社群中,許多人問到為何如此重要,因為 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 是 immutable。"

簡單正確的回答是,由於 => 沒有 this,當然 .bind(obj) 就沒有作用!相同地,=> 箭頭無法用 new 呼叫。由於沒有 thisnew 就沒有東西可以繫結。

理解 => 如何實際處理 this 十分重要,因為這是唯一方式來正確地解釋 => 其他可觀察的行為。當你驚訝時,待在黑暗摸索奇怪的解釋 — 這是不成熟 JS 編程的方式。

總結

不要為了方便而接受不精確的答案。不要用錯誤的方式得到正確的答案。

事情如何做十分重要。你使用哪種精神模範十分重要,因為這個精神模範是你用來分析、描述或除錯其他行為。如果你一開始就走錯方向,你會越錯越遠。

我希望我一開始有聽清楚 Dave 的解說。我希望我沒有不精確的解釋 =>this 的行為。我十分重視我如何思考、撰寫以及教導有關 JS。未來我會更加小心。

Tweet