JavaScript Tips: JavaScript 中的 this

this 的引用問題一直是 JavaScript 新人比較頭疼的問題。前段時間閱讀了方應航老師關于 this 的文章,加深了對 this 的理解。同時,在實際項目中遇到了一些文中沒有提到的關于 this 的用法,特此整理一下。

勘誤:之前發布的文章,將 call 誤寫成了 applycallapply 的效果是一樣的,第一個參數都接收的是函數的 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 。來做一個有點暴力的實驗:

我們嘗試強制指定 foocontextobj2 ,結果顯然 foothis 被綁定為了 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 方法和函數 callcontext 的推導規則。還有就是注意回調函數和閉包的 this ,因為他們的執行可能并沒有經過對象調用,所以很可能丟失 context ,或者指向了別的 context

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容