What’s THIS | 淺談Javascript中令人困擾的this

CHC1024
狗奴工程師
Published in
14 min readMar 22, 2020

--

Photo by sydney Rae on Unsplash

前言

有個主題常常盤據在很多前端工程師的心中:「this究竟是什麼?它本身代表著什麼?」。有時候就算是多年接觸Javascript的老手也不一定全盤理解this在裡頭到底如何運作,它的數值是怎麼來的,為什麼在這種情況下被賦予那樣的值。理所當然地,對於我這個菜鳥工程師也不例外。

關於this這個主題,我自己知道有很多大師級的人物已經發表了對於它的看法與解釋,就是那種你在google上打關鍵字即可撈到一堆大師告訴你this的定義與運作。

我在寫這篇時就在想自己是否有辦法做到像那些大神一樣,把this解釋得非常透徹,還是說就只是關公面前耍大刀呢?畢竟要想把this完全理解,最直白也最原始的方式就是「直接去啃ECMAScript規範」!關於this的規則都是從這個規範來的,背後潛藏一大串的資訊脈絡。因此,我衍生放棄寫這個主題的念頭。

那為什麼最後我還是下海呢?原因有兩個:一個是我認為即便前人真的做過相同的主題,也不代表適合所有人來看,或許我寫的方式反而適合某些新手,等到有想要深入了解this的運作原理時,再去由我推薦的文章中尋找更深遠的答案。另一個原因是來自一年前的目標,就是在畢業與準備重考碩士時,我在閒來無事時都會手賤去碰一下網頁程式,但都不是很深入,否則一頭栽進去就會影響到準備考試,不過有點扯遠了。

總之,當時我注意到自己對於this在Javascript中的意義是一知半解的。過去寫過React多少碰到class中的this,但都以為跟C++與Java類似,唯有寫到bind時才注意到些許的不對勁。因此,我那時就試圖去理解this的內涵,但切入點整個選錯,加上時間分配不足,導致我半途而廢。所以,我想趁著這個機會去完成過去被我放棄的目標,而這就是第二個原因。

總而言之,我希望透過今天這一篇文章,讓新手了解什麼是this。先從this出現的時機,到以我自己對於this的看法來解釋它的數值從何而來。也許身為新手的你看完這一篇能得到啟發,未來面對this時就不會手忙腳亂,能更加有自信地使用它。

從物件導向去看this

我們先來看個實例:

圖一

以上的例子是以物件導向的方式創建一個物件,再去呼叫instance裡面的方法,這種寫法比較接近一般我們在寫C++或是Java時的環境。

我們在這裡宣告了一個類別叫作「Shape」,並且給予三種方法:setLocation、getLocation和toString,每個方法都有取用到this這個關鍵字。

這麼寫的目的是什麼?因為就是沒有其他方法可以在instance內部存取他自己的屬性。更別說Javascript也有所謂的作用域,而且他的機制也與其他語言不太一樣,導致你更不可能省略掉this這個關鍵字去試圖存取內部的屬性。

當然你也可以不要用this當關鍵字,但是重點是我們要找一個字眼代表instance本身。所以關鍵字名稱是什麼根本不重要,只是統一使用this代稱,讓大家學習不同的物件導向程式語言時比較不會混亂。

以上面圖一的範例來說,當我們執行shape.toString()時,該方法內的this就會是shape。所以若你是接觸過物件導向程式語言的人,我想這方面的運作絕對沒問題,因為你會直覺地認為this就是instance本身。

那Javascript的問題在哪?

好的,現在你知道this的出現是為了存取instance內部的屬性,而我們只是找一個關鍵字來代替它。

那現在問題是什麼?有趣的事實是在Javascript中,我們可以在任何地方存取到this。

對,我知道這聽起來非常離譜且不實際,但Javascript就真的是這樣搞。總之,我們就先接受這個設定吧……

在真正接觸Javascript的this前,不少人會認為「阿不就是在class裡面使用?」。正常來說,你不會聽到有人跳出來說C++或Java的this很難理解,對吧?沒錯,你不會聽到任何人抱怨this難用,畢竟他的功能原本是那麼樸實無華且枯燥,是Javascript的出現改變了大家對this的認知。

就此,Javascript裡的this與其他程式語言的this分道揚鑣,走出了自己的路,這才導致很多剛踏入前端的伙伴們感到超級困惑,後來衍生出各種教學文教你如何判別this的值。

但老實說,以大部分的情況而言,我認為this仍然脫離不了物件的框架。更重要的是,當this不是使用在物件導向下使用,他的意義顯得就非常薄弱更可說是沒有意義,這點稍後會提到。

因此,下面的介紹我會以物件為主,因為那才是物件導向的目的。

失去意義的this

在從物件去切入前,容我說明一下脫離物件框架後的this發生什麼事。

首先我們看一張圖:

圖二

你認為hello裡面的this會輸出什麼數值?

希望你不是認為this回傳的是hello這個function本身,因為從物件的觀點來看這很奇怪。總之,一般情況下答案是Window,但我想說其實這沒有什麼太大的意義,所以我們姑且直接說「無意義」。

還記得我前面講的:「this仍然脫離不了物件的框架」嗎?即便我們能夠在任何地方都存取得到this,但在無意義的情形下得到的this值,一般瀏覽器中會是Window,而在NodeJS裡頭則是回傳global。至於如果你開啟嚴格模式,回傳的值一律是undefined,也真的就是沒什麼意義的數值。

稍稍幫你統整一下:

  1. 一般模式 + 瀏覽器 = Window
  2. 一般模式 + NodeJS = global
  3. 嚴格模式 = undefined

總之,this一旦脫離物件導向的世界,他的意義就幾乎消失。因此我們才需要去討論有哪些範疇是屬於物件導向,而this在那裡才顯得重要且有意義。

物件中的this

前面有提到使用Class來創建一個物件,但Javascript還有一個特別的方式可以直接創造物件,如下圖:

圖三

我們不需要使用Class,可以以JSON格式去描述你要的物件即可。
而上面的例子實際跑出來會是John Cena comes back

接著就不得不提及一下在ECMAScript中是如何定義this:

The this keyword evaluates to the value of the ThisBinding of the current execution context.

OK,看起來他的意思是this的值取決於他現行執行的上下文綁定的物件。

沒關係,我們再來看看MDN是怎麼說的:

In most cases, the value of this is determined by how a function is called (runtime binding)

這定義就稍微清楚一點,裡面的大意是我怎麼呼叫function會影響到this的值。
如果一樣看不懂,沒關係,我們直接實際跑一個例子給你看。

我們把圖三的例子做一些擴充:

圖四

這裡我們定義一個函數叫做sayHello,他的內容就是object_example的hello,只是提取他的reference過來。在最外層也定義一個變數叫做name,裡面存’Jack Chen’。

一樣,請猜猜看這兩個輸出值是否相同,若不是,你覺得第二個會輸出什麼?

公布答案囉!
答案是兩個是不同的,而且第二個輸出來的會是「Jack Chen comes back」。

不少新手第一次遇到都會先有一個疑問:「這兩個不是同一個函式嗎?怎麼輸出的值會不一樣呢?」

還記得前面有講到MDN給予的定義嗎?沒錯,this的值來自於我們如何呼叫function。

hello這個函式是依附在object_example上,所以當我們以object_exmaple.hello呼叫時,this就變成object_example,因為我們是藉由它才能使用到hello這個函式。至於sayHello,因為我們使用他時是直接在全域中呼叫,未透過任何物件呼叫函式,所以此時this的值就變為Window(假設使用一般模式&瀏覽器)。

到這裡我把重點稍微整理一下:

1. this的值取決於你如何呼叫function

2. 一旦脫離物件,this就等於在全域呼叫函式

一秒判別this

上面講了那麼多,那有沒有一種方法可以馬上判斷this的值呢?當然有,而這個方法是我從別的大神那邊學來的,有興趣的人可以參考《this 的值到底是什么?一次说清楚》

文章的大意是一般我們使用函式的寫法都是call的語法糖,所以我們可以把所有function call轉換成用call來解釋。以圖四的例子來說,我們可以做以下的轉換:

圖五

簡單來說,規則就是「function name之前有什麼東西,那call的第一個參數就放什麼」。

以上述的規則來說,我們就會把object_example當作call的參數;而sayHello的前面什麼都沒有,所以就直接空白即可。

既然知道有這樣的規則,這裡列舉一般會遇到this的五種情境:

  1. 全域函式
  2. 單層物件函式
  3. 巢狀物件函式
  4. 巢狀函式
  5. 函式陣列

首先,前兩種我們已經討論過了,所以不再贅述。
先舉一個巢狀物件的例子:

圖六

想一下,那三行function call分別會輸出什麼值以及若以call來書寫會長什麼樣子。

準備好的話我就直接公布答案,還沒想好的人可以先暫停在這裡呦!

圖七-解答

很簡單對不對?只要記住我們上面說的:「this的值取決於你如何呼叫function」,而這個「如何呼叫」就是當你呼叫時,function name前面有什麼,就把他放進call的第一個參數。

接著我們就來解決剩下兩個情境。不過事前先打個預防針,以下的兩種情況與前三者會有點不同,尤其是函數陣列需要一點想像力。

巢狀函式

按照慣例,先放一張的情境圖:(假設是瀏覽器的一般模式)

圖八

在答題之前,請千萬記住轉換的規則與開頭我們講到:「一旦脫離物件的框架,this就無意義」。

如果你確定有答案之後,再往下看解答。

圖九-解答

如果你輕鬆答對,那恭喜你真的搞懂我前面說的兩件事情。那假如你答錯也不用氣餒,一開始我也是答錯的。下面讓我告訴你為什麼他的輸出會是2。

hello2被定義在hello中,所以當我們執行obj.hello時,在hello就會一併執行hello2。你也許會認為hello2中的this應該要和hello一樣,但看看第一準則「this必須與物件連結才有意義」,你就會發現hello2事實上並沒有加入任何一個物件,只是方法中定義的一個變數(函數)。

既然他不是一個物件的方法,那我們就直接把他做轉換,會得到hello2.call(),也就是說this會變成Window。因此,this.value就會等價於Window.value,所以整個輸出才會是2。

函數陣列

終於來到最後一里路了,只要過了這關,八成的情況你都能解決。準備好的話,我們就繼續囉!GOGO!

直接上題目:

圖十

請先自己想想看,這兩行輸出會是什麼。

如果你認為答案是John Cena20,那恭喜你答錯了!
事實上兩個輸出都是undefined,我知道這很難理解,但且聽我道來。

一般人的思路會是:arr[0]等於func1,所以當我執行arr[0]()就等於執行func1(),也就等價於執行全域函數。因此,this就是Window,而this.name就是John Cena

然而,真正的回傳值卻是undefined,這又是為什麼呢?以下有兩種解釋:

  1. 將Array假想成JSON格式物件

一樣引自於《this 的值到底是什么?一次说清楚》的解釋。

簡單來說,我們可以把arr[0]假想成arr.0,其中的「0」就是function name,所以當我們執行arr[0](),就等於執行arr.0()。

然後就是……

我們就能快樂地把arr.0()轉換成arr.0.call(arr)。

2. 從記憶體的觀點出發

不論是什麼變數,我們都必須將他們儲存在記憶體當中。當我們把一個變數放進陣列中,其實我們是把它的參考(reference)存進格子(entry)。

當我們執行arr[0](),實際上會先去arr的記憶體空間找第一個格子,接著依循格子所指向的位址找到function,最後才得以執行該function。因此,一切都是從arr開始,所以this才會是arr,因為我們是從arr的記憶體空間找到這些函式,是透過arr才得以執行函式。

綜合以上,雖然不是那麼正式的解釋,但透過一些想像力,你就能知道當我們執行函數陣列裡的任一個函數,裡頭的this就會等於該陣列。以我們的例子就會是arr,而因為arr中並未定義name跟age,才導致上面的例子最後輸出的值是undefined

學到這裡,不論是幾維陣列你都能回答了,如果覺得沒自信,我們這就直接來一題:

圖十一

只要按照前面的思考方式將陣列轉換成物件(或是以記憶體解讀),我們就會得到:

圖十二-解答

是不是很簡單呢?只要把握一兩個原則,我們就能輕鬆知道this的值。

總結

對於this,如果有人問我要如何知道他的值,我會如此回答:

1. 一旦脫離物件的框架,this就會變成無意義

2. 沒有意義的this會根據環境給予一個預設值,但不重要

3. 有意義的this會根據函式如何被呼叫,將它指向一個物件

4. 所有function call皆可轉換成call的形式。轉換的規則是將function name前的東西當作call的第一個參數,而那就是this

靠著以上四個準則,我想可以應付八成的狀況。剩下的兩成,像是箭頭函式,情況就稍微不一樣了,有時間的話,我想我會另外再開一篇來聊一下this是如何在裡面運作的。至於事件處理,雖然也會使用到的this,但我認為看完這一篇,你應該就能舉一反三。

總之,這篇文章是將我最近看到的幾篇文給我的感受融合起來,也灌注我自己消化過後對於this的想法。
如果你是卡在this很久,不知道該怎麼理解它的新手,希望透過這一篇能給你一些新的思路,讓你更加了解this的意義與運作。

後記

在寫這篇的過程中,再次體會到Medium非常不適合貼程式碼,尤其是單純貼演示的程式碼。如果今天是想要展現一下HTML+CSS,我還可以使用Codepen來輔助展示成果。然而,單純以JS來說是很難呈現給讀者的,所以我才選擇以圖片的方式將程式碼貼上來。

一開始我是自己用截圖軟體幫我截圖,但後來發現圖片的畫質有點糟糕,因此作罷。後來我在網路上找到一個網頁能夠幫我直接將程式碼轉換成圖片,叫做Carbon,你唯一需要做的是專注在寫程式上。它也有提供客製化的版面供你做設定,完成編輯後就可以直接輸出成png檔以供使用。

總之,如果不想花時間在Medium編輯器上做排版,或是拉Codepen到你的文章中的話,我推薦可以使用Carbon幫你省下一些時間,同時也可以做出好看的程式碼截圖。不過未來可能還是得將文章轉戰到其他平台上,Medium不適合刊登需要貼程式碼的文章,樣式實在太少了。

延伸閱讀

  1. this 的值到底是什么?一次说清楚
  2. What’s THIS in JavaScript ?
  3. JavaScript深入之从ECMAScript规范解读this
  4. 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂

--

--