[教學] JavaScript this 用法整理

MowLi 微風
微風飛翔
Published in
8 min readMar 13, 2021

本文章主要說明 Javascript this 的用法,提供了程式碼範例及說明,不僅是入門或者進階的開發者都值得一讀。

this 在 JavaSrcipt 中預設值是指向全域,也因為這一項設定造成寫作上的困擾。本文章參考了「你所不知道的 JS詳細講述 this 基本使用與衍生的內部原理。

Photo by Jamie Templeton on Unsplash

一、直接呼叫的函式,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 的情況:

範例一:沒有使用 var self = this 的情況

我們在 obj 中的 update 函式裡面又宣告了 setName 函式,setName 函式預計作用是去設置一個新名稱給 obj 的 name 變數。

第 13 行呼叫 obj.update()。首先,因為是以物件去呼叫函式,因此第 4 行顯示了「初三睡到飽」,而我們在第 8 行傳入了「初四了,阿伯」,但是結果並非預期,obj 的 name 變數沒有被更改。

在前面我們已經說明了原因,因為這是一個直接呼叫的函式,因此「this 會指向全域」,圖 1 證明 window 物件裡面多了一個 name 變數。

圖1 預設繫結與全域物件

此時 var self = this 就派上用場:

範例二:var self = this 的情況

我們在第 4 行用 self 儲存 this 的參考,並在第 7 行中將 this.name 更改成 self.name,所以即使第 9 行直接呼叫函式的情況下,還是能透過 self 來存取到原本的 this。

哦!那箭頭函式要怎麼達成以上的效果?那又更簡單了,直接用就對了:

優先順序

關於 this 的五種用法具有優先順序,依照順序大小分別為:

  1. ES6 箭頭函式
  2. new 關鍵字
  3. call, apply, bind
  4. 哪個物件去呼叫了函式,函式內的 this 就會指向該物件
  5. this 指向全域

換句話說,例如使用 call, apply, bind 時,你必須先注意到前面是否有 new 的存在,因為 new 的優先權較大,會把 call, apply, bind 的用法蓋過去。

關於測試的程式碼就不作詳述,此處表達的重點是必須了解到 this 的用法具有優先順序即可。

其他重要事項

  1. 嚴謹模式 (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 用法,索性就用箭頭函式解決了。

參考資料

  1. 你所不知道的 JS:範疇與 Closures/this 與原型物件
  2. MDN Web Docs — Function.prototype.call
  3. JavaScript 原型鏈中應該了解的概念

--

--