this
的引用問題一直是 JavaScript 新人比較頭疼的問題老速。前段時(shí)間閱讀了方應(yīng)航老師關(guān)于this
的文章古今,加深了對(duì)this
的理解。同時(shí)秆撮,在實(shí)際項(xiàng)目中遇到了一些文中沒有提到的關(guān)于this
的用法他宛,特此整理一下船侧。
勘誤:之前發(fā)布的文章,將
call
誤寫成了apply
厅各。call
和apply
的效果是一樣的镜撩,第一個(gè)參數(shù)都接收的是函數(shù)的context
。只不過apply
將函數(shù)的參數(shù)變成了數(shù)組進(jìn)行傳遞而已队塘。
上下文環(huán)境( context )
簡(jiǎn)單來講袁梗,上下文環(huán)境指的是當(dāng)前代碼片段(函數(shù))運(yùn)行時(shí)所處的環(huán)境。
在 JavaScript 中人灼,每一個(gè)函數(shù)在執(zhí)行的時(shí)候都會(huì)被賦予一個(gè) context
围段,即函數(shù)運(yùn)行的上下文環(huán)境(執(zhí)行上下文),這個(gè)環(huán)境通常是一個(gè)對(duì)象投放。在函數(shù)中奈泪,我們使用 this
訪問函數(shù)的執(zhí)行上下文。這個(gè)上下文環(huán)境隨著函數(shù)的調(diào)用方式灸芳、形式涝桅、位置等的不同會(huì)發(fā)生變化,因此我們無法直接依賴函數(shù)聲明時(shí)的上下文環(huán)境來進(jìn)行某些操作烙样。這其中最典型的就是 setTimeout
這樣的異步函數(shù)冯遂。
舉幾個(gè)簡(jiǎn)單的例子,來觀察一下函數(shù)的執(zhí)行上下文:
-
這是一個(gè)定義在全局環(huán)境的函數(shù)
foo
谒获,我們?cè)谌汁h(huán)境中調(diào)用它:
得到了全局對(duì)象
Window
蛤肌,蠻合理的。(當(dāng)然這么理解是不完全正確的批狱,慢慢往下看) -
我們定義一個(gè)
obj
對(duì)象裸准,其中的foo
屬性指向剛才定義的foo
函數(shù):
此時(shí)雖然
obj
中的foo
直接指向了全局foo
函數(shù),但是其執(zhí)行結(jié)果卻變成了obj
對(duì)象赔硫。 -
我們反過來再試一下:
結(jié)果也反過來了炒俱。
更難受的是setTimeout
這樣的方法:
由此證明,函數(shù)的執(zhí)行上下文與函數(shù)聲明時(shí)的上下文不一定相同。所以我們有的時(shí)候會(huì)看到這樣的寫法权悟,用來保存函數(shù)依賴的上下文環(huán)境:
為什么會(huì)有這種差別呢砸王?
在 JavaScript 中,“萬物皆對(duì)象”峦阁。每一個(gè) function
其實(shí)是由 Function
類生成的一個(gè)對(duì)象谦铃。在執(zhí)行函數(shù)調(diào)用時(shí),其實(shí)是執(zhí)行了一個(gè)語法糖拇派,真正被調(diào)用的是函數(shù)的 call
內(nèi)置方法荷辕。這個(gè)方法接收兩種參數(shù):call(context, [arg1, [arg2..)
。context
便是這個(gè)函數(shù)執(zhí)行的上下文件豌,即 this
。來做一個(gè)有點(diǎn)暴力的實(shí)驗(yàn):
我們嘗試強(qiáng)制指定 foo
的 context
為 obj2
控嗜,結(jié)果顯然 foo
的 this
被綁定為了 obj2
茧彤。
而 JavaScript 又是如何執(zhí)行這個(gè)語法糖的呢?我們肯定會(huì)這么猜:JavaScript 會(huì)自動(dòng)向前調(diào)用這個(gè)函數(shù)的的對(duì)象疆栏,并將這個(gè)對(duì)象作為 context
再執(zhí)行 call
曾掂。這樣說并沒有錯(cuò),但是不全面壁顶,來看下邊幾個(gè)實(shí)驗(yàn):
-
先創(chuàng)建一個(gè)
father
對(duì)象:
顯然這個(gè)是符合我們猜想的珠洗。
-
然后我們?cè)賱?chuàng)建一個(gè)
child
對(duì)象:
顯然也符合我們的猜想。
-
現(xiàn)在我們把兩個(gè)對(duì)象結(jié)合起來:
想必和一些人猜想的不一樣吧若专。
JavaScript 只會(huì)尋找最終調(diào)用該函數(shù)的對(duì)象许蓖,而不會(huì)向前追溯。
不過還有一個(gè)問題调衰,為什么直接執(zhí)行函數(shù)的時(shí)候膊爪,會(huì)輸出 window
這個(gè)對(duì)象。是因?yàn)樵跒g覽器中所有的對(duì)象都是 window
的屬性嚎莉,所以 foo()
等價(jià)于 window.foo()
嗎米酬?答案是否定的。
在 JavaScript 中趋箩,如果函數(shù)是直接調(diào)用的赃额,而不是源自于某個(gè)對(duì)象,函數(shù)的 call
方法的 context
將會(huì)被定義成 undefined
叫确。所以 foo()
與 foo.call(undefined)
是完全等價(jià)的:
在瀏覽器策略中跳芳,函數(shù) context
如果為 undefined
,將會(huì)自動(dòng)綁定全局對(duì)象 window
启妹。這種綁定在 JavaScript 嚴(yán)格模式下會(huì)被禁止筛严。
按照規(guī)矩來也不行?
有些寫在函數(shù)里的函數(shù)(或者說,閉包)桨啃,會(huì)丟失原函數(shù)的上下文车胡。其實(shí)也不怪它,因?yàn)楹瘮?shù)的執(zhí)行上下文是不會(huì)繼承的:
如果你理解了剛才對(duì) call
的解讀照瘾,你也許就會(huì)認(rèn)為:inner
并沒有被任何對(duì)象調(diào)用匈棘,而是直接被執(zhí)行了,自然會(huì)丟失上下文析命。這樣的理解在這個(gè)例子中是正確的主卫,但是當(dāng)函數(shù)作為回調(diào)時(shí)會(huì)復(fù)雜一些。
回調(diào)函數(shù)的調(diào)用方式與回調(diào)函數(shù)的執(zhí)行者有關(guān)鹃愤,其 this
與執(zhí)行者執(zhí)行函數(shù)時(shí)為其指定的 context
有關(guān)簇搅。沒有指定 context
的結(jié)果與上邊的結(jié)果是一致的,但指定了 context
的就不一定了软吐,要仔細(xì)閱讀文檔瘩将。
關(guān)于 bind
很多時(shí)候,由于執(zhí)行者的不可靠性凹耙,或者其他的原因姿现,我們想為函數(shù)手動(dòng)綁定 context
。JavaScript 為我們提供了 bind
方法肖抱,返回一個(gè)綁定了上下文的函數(shù)备典,來改寫一下上邊出現(xiàn)的 setTimeout
的例子:
特殊語法:[]
function fn () {
console.log(this)
}
var arr = [fn]
arr[0]()
這樣的函數(shù)調(diào)用,調(diào)用對(duì)象是數(shù)組 arr
本身意述,所以它將被作為 context
傳入:
箭頭函數(shù)
ES6 為了解決 this binding 這個(gè)讓人非常頭疼的問題提佣,提供了一種新的函數(shù)聲明方式:箭頭函數(shù)。箭頭函數(shù)會(huì)自動(dòng)綁定函數(shù)聲明時(shí)所在的上下文的 this
欲险。關(guān)于箭頭函數(shù)具體的信息可以查閱箭頭函數(shù) | MDN
我們可以用箭頭函數(shù)改寫上面出現(xiàn)的 setTimeout
的實(shí)驗(yàn):
總結(jié)
函數(shù)的 this
最核心的地方就是掌握函數(shù)的 call
方法和函數(shù) call
的 context
的推導(dǎo)規(guī)則镐依。還有就是注意回調(diào)函數(shù)和閉包的 this
,因?yàn)樗麄兊膱?zhí)行可能并沒有經(jīng)過對(duì)象調(diào)用天试,所以很可能丟失 context
槐壳,或者指向了別的 context
。