this
可以在 JavaScript 當中可以說是比 prototype
還要更難懂的概念,但今天要來嘗試以簡潔的方式來說明 this
到底是誰。
this
簡單來講,this
依據的是 函式被呼叫的方式,而呼叫方式可分為:
- 預設綁定:全域呼叫、呼叫當下參考沒有自物件的函式。
- 隱含綁定:呼叫當下參考自物件的函式。
- 被綁定的呼叫:使用 bind、call、apply。
- 箭頭函式的呼叫。
- 函式建構式的呼叫。
預設綁定、隱含綁定
隱含綁定主要是依據 執行當下該行程式碼時有沒有參考物件,如果沒有參考物件預設則會自動綁定全域物件(在瀏覽器是 window
,在 Node.js 是 global
)。
1 | function getX (){ |
getX()
該行並沒有參考物件才去呼叫,所以 this
自動綁定全域物件 window,最後找到的是 window.x 的 10
此外如果是嚴格模式(strict mode),如果採用預設綁定, this
將不會自動綁定全域物件,而是給予 undefined
1 | ; |
現在來嘗試看看複雜例子並且想一下答案是什麼:
1 | function getX (){ |
getX()
:10,因為參考時純粹拿到了該函式並且執行。
obj.getX()
:20,透過 obj 物件去尋找該方法並呼叫,所以是指向 obj,因此是 obj.x 的值。
obj2()
:10,這裡有個陷阱,實際上該行並沒有參考物件,而是透過一個已經指向getX
函式的參考去呼叫的
所以簡單的來說,最主要是專注在執行的該行即可,執行該行所產生的執行環境,this
就綁在它上面!
再來一題:
1 | function getX (){ |
getX()
:10,剛行沒參考自物件,所以this
預設綁定在全域上。obj.getX()
:20,剛行參考自物件obj
,所以this
預設綁定在obj
上。obj2.getX()
:30,剛行參考自物件obj2
,所以this
預設綁定在obj2
上。obj3()
:10,剛行沒參考自物件,所以this
預設綁定在全域上。obj4.getX()
:40,剛行參考自物件obj4
,所以this
預設綁定在obj4
上。
最後一題:
1 | var x = 10 |
obj.getX()
:答案為 10。
剛才說到了執行該行所產生的執行環境,this
就綁在它上面,所以最後 console.log
前所執行的函式是哪句?
如果你看成 obj.getX()
,理所當然的你就會不小心答出 20
。
而其實最後執行 console.log
的函式是 inner()
,而 inner()
該行並沒有參考其他物件,所以是預設綁定在全域中,也就是 window
,因此答案是 10
。
預設綁定、隱含綁定實戰中的問題
有時候我們呼叫函式時不想使用當下的 this
而是想要外層的 this
怎麼辦?
1 | function wait(second){ |
在上面這段程式中我們雖然最底下執行當下有參考到 obj
了,但 wait
函式中真正在執行 console.log()
前呼叫的是 window.setTimeout
這個函式,所以其實是參考了 window
這個全域物件並把 this
綁定給他,最後拋出 window.x
的值,也就是 undefined
。
所以用 console.log(this)
檢查其實會像這樣:
1 | function wait(second){ |
確認 this
的範疇後,我們就可以利用作用域的特性(找不到變數值會向外層找)來實現抓取外面的 this
:
1 | function wait(second){ |
至於那個 that
主要是用來儲存 this
指向用的,另外也有人會取名為 self
等等名稱。
如果這個方法還不喜歡的話,可以繼續往下看看其他綁定方法。
強制綁定 bind、call、apply
除了上面這種預設綁定與隱含(implicit)綁定之外,接下來要介紹強制綁定的三種方法。
bind
bind
的用法主要是在呼叫時加上綁定 this
的物件對象:
1 | function getX(){ |
原本 getX()
執行到該行時會由於該行當下沒有去其他物件中尋找函式,所以會採預設綁定在 window
當中,但透過 bind
的綁定,我們可以在呼叫時將 this
指定給另一個物件,因而可以參考到不同的物件當中。
上面所提到的實戰的部分也可以用 bind
解決
1 | function wait(second){ |
上面的程式範例中,我們透過 bind
綁定了 setTimeout
裡面的 this
,因此每次呼叫 wait
時,裡面 setTimeout
中回呼函式內的 this
都會與外面相同了,而不會被 window.setTimeout
的 window
物件所影響到。
call、apply
call
、apply
的用法比較接近,差別在使用時會立即呼叫該函式:
1 | function getX(num = 0, anotherNum = 0){ |
call(),第一個參數是要綁定
this
的物件,後續帶入參數是用逗號來區隔。
apply(),第一個參數也是綁定
this
的物件,但後續帶入參數是放在一個陣列當中。
箭頭函式的呼叫
箭頭函式(arrow function)本身並沒有 this
,並且會遵循一般變數查找的邏輯來運作,因此在箭頭函式中的 this
如同綁定在函式宣告之處:
1 | function getX(){ |
第一個 setTimeout
我們在上集已經討論過它是綁定到全域當中,所以最後會撈到 window.x
。
然而第二個 setTimeout
中的回呼函式用了箭頭函式的寫法,因此在即時直譯的過程中在箭頭函式內是找不到 this
的,接著他會如同我們在找變數值所參考作用域的情況一樣,向外層作用域去尋找 this
,最後在 getX
作用域找到了當下執行環境中的 this
,也就是由 obj.getX()
所創造出來的 this
。(如第一個 console.log
當下的作用域一樣)
現在我們便知道為何用在箭頭函式中的 this
如同綁定在函式宣告之處的這個由來了。
箭頭函式的呼叫 in 嚴格模式
箭頭函式中的 this
另一個值得一提的就是在嚴格模式下,以往嚴格模式是禁止預設綁定到全域當中,並且會給予 undefined
:
1 |
|
但如果是使用箭頭函式的話就沒有這個限制:
1 |
|
以上就是箭頭函式對於 this
的影響,最後再來談談個 new
。
new 的綁定
new
關鍵字主要是用來初始化函式建構式,而使用 new
的當下,this
就會綁定在對應的物件上:
1 | function Fruit(name) { |
雖然不是很完整,到這邊為止 this
的觀念已經可以解決大部分一般的 this
問題了。
如果還想繼續深究
this
的話我只能推這篇文章了