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