ES6 箭頭函式中的 this?(臨時性儲存)
是否區域性(Lexical)?
包括我在內的許多人,都會這麼描述箭頭函式裡 this 的行為:區域性的 this。什麼意思呢?
<body>
<div id="demo" onclick="foo()" > 點我</div>
<script>
document.getElementById('demo').onclick =function(){
console.log(this.tagName);//div
setTimeout(function(){
console.log(this );//window
},1000)
}
</script>
</body>
function foo() {
setTimeout( () => {
console.log("id:", this.id);
},100);
}
foo.call( { id: 42 } );
// id: 42
這裡的 => 箭頭函式看起來把它內部的 this 繫結為父函式 foo() 裡的 this。如果這個內部函式是一個常規的函式(宣告或表示式),它的 this 將類似 setTimeout 如何呼叫函式一樣被控制著。
區域性變數 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的時候更加嚴格地確保其正確性,也會讓自己更加小心謹慎。