本文章主要說明 Javascript this 的用法,提供了程式碼範例及說明,不僅是入門或者進階的開發者都值得一讀。
this 在 JavaSrcipt 中預設值是指向全域,也因為這一項設定造成寫作上的困擾。本文章參考了「你所不知道的 JS」,詳細講述 this 基本使用與衍生的內部原理。
一、直接呼叫的函式,this 會指向全域
也就是說,在一個函式(物件)裡面直接讀取 this 關鍵字的變數時,則會到最外層的區域(全域)尋找同名的變數,若沒有,則會在全域中直接新增變數。
以上範例在「全域」中及「函式」分別宣告了變數
name
,有些人會認為既然 this.name 在 who 函式內,那麼 name應該會顯示「大家好,這是函式 name」。但實際上並非如此,直接呼叫的函式其 this 會指向全域。
二、哪個物件去呼叫了函式,函式內的 this 就會指向該物件
換句話說,當一個函式被包含在某一個物件中,則該函式裡面的 this 是指向該物件。
如下程式碼,obj1 物件中的 name 叫做「微風」,obj2 物件中的 name為「狐狸」,而 who 函式的作用會顯示出名稱 console.log(this.name)。
因為是 obj1 呼叫了 who(),所以函式內的 this 就會指向 obj1,顯示出「微風」;同理,obj2 呼叫函式時則會顯示「狐狸」。
但是,有時候「哪個物件去呼叫了函式,函式內的 this 就會指向該物件」會失效,反而變成 「this 指向了全域」,造成非預期的錯誤
主要有以下兩種失效的情況:
1. 間接呼叫
把物件中的方法參考到另一個新變數(call reference),當呼叫這個新變數時就會退回到「this 指向全域」的模式。以程式碼為例:
其實可以用前面介紹的思維來解釋,
loseImplicitlyBound()
是一個直接被呼叫的函式,因此 this 很自然地會指向全域,顯示在全域宣告的name
值「大家好,這是全域 name」,而非 obj1 的「微風」。
2. callback
比較恐怖的是,使用 callback 時也有可能會發生「this 指向全域」的情況。如以下程式碼:
我們在
callbackTest()
中傳入了obj1.who
,而callbackTest()
會在裡面呼叫obj1.who
,理論上結果應該會顯示「微風」,卻變成了「大家好,這是全域 name」。
因此,為了解決上述發生的問題,才會出現了第三種 this 用法,直接以 call, apply, bind 綁定 this 指向誰。
三、call, apply 會將第一個參數作為 this 綁定的目標
而兩者方法主要差別只是在於 call()
後面接收一連串的參數,而 apply()
則接收一個陣列作為參數。如下:
showTrainInfo 函式會印出 this 及 arg 的資訊,我們使用 call, apply 把 this 綁定到 train 物件,所以結果顯示了 train 物件的值「高雄 到 台南 」,而第 11 行 call() 傳入了個別參數,第 13 行 apply() 傳入了陣列,兩者同樣印出了「 30 分鐘 區間車」。
強制綁定
但是,call(), apply() 會有被覆蓋的風險,導致 this 仍然有可能指向全域,例如使用第三方函式庫之類的,比較保險作法是使用「強制綁定」:
- 透過兩種方式達成: bind() 或函式表示式(Function Expression)
與 call, apply 不同,bind 會直接回傳一個綁定好的函式,可以用變數去接收及使用(第 16 行)。另外,使用函式表示式也能達到 bind 的效果(第 20 行)。
- 由硬繫結所綁定的 this 只能綁定一次,不能重新綁定
經過前一步驟的綁定,發現後來這個函式無論使用 call, apply, bind 都無法重新綁定。因此,使用第三方函式庫時,比較不用怕綁定的機制莫名其妙被蓋掉。
四、this 會綁定在使用 new 關鍵字所建立的實體物件
在函式前面加上 new 關鍵字,this 會綁定在函式所建立出來的實體物件 result 上,所以存取 result.name 會有當初在 new 所傳入的值「大家好,我是 new」。
五、箭頭函式內的 this 會找到離自己最近的範疇 (區域或全域) 作為綁定對象
在說明箭頭函式前,我們先來了解一段歷史:
先前提到「直接呼叫函式」會讓 this 指向全域,造成某些使用情況下的困擾,在箭頭函式還沒推出時,除了使用 call, apply, bind,有些人會以「var self = this」的方式,先把外層的 this 儲存起來,然後放到函式使用。
你可能會疑惑幹嘛提到 var self = this
?這是因為var self = this
的原理類似於箭頭函式。以下直接看程式碼來說明運作,第一個範例是沒有使用 var self = this
的情況:
我們在 obj 中的 update 函式裡面又宣告了 setName 函式,setName 函式預計作用是去設置一個新名稱給 obj 的 name 變數。
第 13 行呼叫 obj.update()。首先,因為是以物件去呼叫函式,因此第 4 行顯示了「初三睡到飽」,而我們在第 8 行傳入了「初四了,阿伯」,但是結果並非預期,obj 的 name 變數沒有被更改。
在前面我們已經說明了原因,因為這是一個直接呼叫的函式,因此「this 會指向全域」,圖 1 證明 window
物件裡面多了一個 name
變數。
此時 var self = this
就派上用場:
我們在第 4 行用
self
儲存 this 的參考,並在第 7 行中將 this.name 更改成 self.name,所以即使第 9 行直接呼叫函式的情況下,還是能透過self
來存取到原本的 this。
哦!那箭頭函式要怎麼達成以上的效果?那又更簡單了,直接用就對了:
優先順序
關於 this 的五種用法具有優先順序,依照順序大小分別為:
- ES6 箭頭函式
- new 關鍵字
- call, apply, bind
- 哪個物件去呼叫了函式,函式內的 this 就會指向該物件
- this 指向全域
換句話說,例如使用 call, apply, bind 時,你必須先注意到前面是否有 new 的存在,因為 new 的優先權較大,會把 call, apply, bind 的用法蓋過去。
關於測試的程式碼就不作詳述,此處表達的重點是必須了解到 this 的用法具有優先順序即可。
其他重要事項
- 嚴謹模式 (strict)
第一種用法(直接呼叫函式)在非嚴謹模式下會回傳 undefined
;而在嚴謹模式(strict)下則會回報錯誤。
因此,設置嚴謹模式有一個好處:避免不小心將 this 指向全域時阻止我們改動到全域的東西,以利於除錯或發生奇怪、非預期的現象發生。
2. 使用 call, apply, bind 時不要將 this 綁定給 null,會導致「this 指向全域」
讓我們來釐清什麼時候會把 this 綁給 null
?通常是當你不在意 this 綁定給誰,只想把值給攤開 (apply) 或是用 currying 將底層的值設定好時,就有可能這麼做。
解決辦法可以運用原型鏈[3]中的 Object.create(null)
來創造一個乾淨的物件用來替代 null
。
牛刀小試
猜猜看以下執行結果為何?
結語
本文介紹了五種 this 的用法以及各個 this 用法的權重順序,也提到幾個可能導致 this 指向全域的恐怖情況,希望讀者在閱讀後能覺得對 JavaScript 中的 this 能有更進一步的理解,避免掉一些奇怪的 Bug。
另外,也蠻推大家看這本書,覺得後面關於 OOP、原型鏈 OOP、OLOO 的章節寫得蠻精彩的。
小插曲 — callback
筆者最近在學習 Vue 時也遇到了同樣問題,簡單說明一下程式碼,本來我想用 Axios 去撈取伺服器上 json 資料,並把資料同步到 Vue 的組件上,讓進入頁面的使用者能看到對應的頁面標題及資料等。
在 .then()
的地方我傳入了一個 callback 來存取資料,但此時 this 已被指向全域,因此無法正常修改到 Vue 中的 title
, description
變數,反而在全域中另外建立了 title
, description
變數。還好那時正好剛讀完了 this 用法,索性就用箭頭函式解決了。
參考資料
- 你所不知道的 JS:範疇與 Closures/this 與原型物件
- MDN Web Docs — Function.prototype.call
- JavaScript 原型鏈中應該了解的概念