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的話我只能推這篇文章了