this
的引用問題一直是 JavaScript 新人比較頭疼的問題。前段時間閱讀了方應航老師關于this
的文章,加深了對this
的理解。同時,在實際項目中遇到了一些文中沒有提到的關于this
的用法,特此整理一下。
勘誤:之前發布的文章,將
call
誤寫成了apply
。call
和apply
的效果是一樣的,第一個參數都接收的是函數的context
。只不過apply
將函數的參數變成了數組進行傳遞而已。
上下文環境( context )
簡單來講,上下文環境指的是當前代碼片段(函數)運行時所處的環境。
在 JavaScript 中,每一個函數在執行的時候都會被賦予一個 context
,即函數運行的上下文環境(執行上下文),這個環境通常是一個對象。在函數中,我們使用 this
訪問函數的執行上下文。這個上下文環境隨著函數的調用方式、形式、位置等的不同會發生變化,因此我們無法直接依賴函數聲明時的上下文環境來進行某些操作。這其中最典型的就是 setTimeout
這樣的異步函數。
舉幾個簡單的例子,來觀察一下函數的執行上下文:
-
這是一個定義在全局環境的函數
foo
,我們在全局環境中調用它:
得到了全局對象
Window
,蠻合理的。(當然這么理解是不完全正確的,慢慢往下看) -
我們定義一個
obj
對象,其中的foo
屬性指向剛才定義的foo
函數:
此時雖然
obj
中的foo
直接指向了全局foo
函數,但是其執行結果卻變成了obj
對象。 -
我們反過來再試一下:
結果也反過來了。
更難受的是setTimeout
這樣的方法:
由此證明,函數的執行上下文與函數聲明時的上下文不一定相同。所以我們有的時候會看到這樣的寫法,用來保存函數依賴的上下文環境:
為什么會有這種差別呢?
在 JavaScript 中,“萬物皆對象”。每一個 function
其實是由 Function
類生成的一個對象。在執行函數調用時,其實是執行了一個語法糖,真正被調用的是函數的 call
內置方法。這個方法接收兩種參數:call(context, [arg1, [arg2..)
。context
便是這個函數執行的上下文,即 this
。來做一個有點暴力的實驗:
我們嘗試強制指定 foo
的 context
為 obj2
,結果顯然 foo
的 this
被綁定為了 obj2
。
而 JavaScript 又是如何執行這個語法糖的呢?我們肯定會這么猜:JavaScript 會自動向前調用這個函數的的對象,并將這個對象作為 context
再執行 call
。這樣說并沒有錯,但是不全面,來看下邊幾個實驗:
-
先創建一個
father
對象:
顯然這個是符合我們猜想的。
-
然后我們再創建一個
child
對象:
顯然也符合我們的猜想。
-
現在我們把兩個對象結合起來:
想必和一些人猜想的不一樣吧。
JavaScript 只會尋找最終調用該函數的對象,而不會向前追溯。
不過還有一個問題,為什么直接執行函數的時候,會輸出 window
這個對象。是因為在瀏覽器中所有的對象都是 window
的屬性,所以 foo()
等價于 window.foo()
嗎?答案是否定的。
在 JavaScript 中,如果函數是直接調用的,而不是源自于某個對象,函數的 call
方法的 context
將會被定義成 undefined
。所以 foo()
與 foo.call(undefined)
是完全等價的:
在瀏覽器策略中,函數 context
如果為 undefined
,將會自動綁定全局對象 window
。這種綁定在 JavaScript 嚴格模式下會被禁止。
按照規矩來也不行?
有些寫在函數里的函數(或者說,閉包),會丟失原函數的上下文。其實也不怪它,因為函數的執行上下文是不會繼承的:
如果你理解了剛才對 call
的解讀,你也許就會認為:inner
并沒有被任何對象調用,而是直接被執行了,自然會丟失上下文。這樣的理解在這個例子中是正確的,但是當函數作為回調時會復雜一些。
回調函數的調用方式與回調函數的執行者有關,其 this
與執行者執行函數時為其指定的 context
有關。沒有指定 context
的結果與上邊的結果是一致的,但指定了 context
的就不一定了,要仔細閱讀文檔。
關于 bind
很多時候,由于執行者的不可靠性,或者其他的原因,我們想為函數手動綁定 context
。JavaScript 為我們提供了 bind
方法,返回一個綁定了上下文的函數,來改寫一下上邊出現的 setTimeout
的例子:
特殊語法:[]
function fn () {
console.log(this)
}
var arr = [fn]
arr[0]()
這樣的函數調用,調用對象是數組 arr
本身,所以它將被作為 context
傳入:
箭頭函數
ES6 為了解決 this binding 這個讓人非常頭疼的問題,提供了一種新的函數聲明方式:箭頭函數。箭頭函數會自動綁定函數聲明時所在的上下文的 this
。關于箭頭函數具體的信息可以查閱箭頭函數 | MDN
我們可以用箭頭函數改寫上面出現的 setTimeout
的實驗:
總結
函數的 this
最核心的地方就是掌握函數的 call
方法和函數 call
的 context
的推導規則。還有就是注意回調函數和閉包的 this
,因為他們的執行可能并沒有經過對象調用,所以很可能丟失 context
,或者指向了別的 context
。