前言
有個主題常常盤據在很多前端工程師的心中:「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
,也真的就是沒什麼意義的數值。
稍稍幫你統整一下:
- 一般模式 + 瀏覽器 = Window
- 一般模式 + NodeJS = global
- 嚴格模式 = 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的五種情境:
- 全域函式
- 單層物件函式
- 巢狀物件函式
- 巢狀函式
- 函式陣列
首先,前兩種我們已經討論過了,所以不再贅述。
先舉一個巢狀物件的例子:
想一下,那三行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 Cena
跟20
,那恭喜你答錯了!
事實上兩個輸出都是undefined
,我知道這很難理解,但且聽我道來。
一般人的思路會是:arr[0]等於func1,所以當我執行arr[0]()就等於執行func1(),也就等價於執行全域函數。因此,this就是Window
,而this.name就是John Cena
。
然而,真正的回傳值卻是undefined,這又是為什麼呢?以下有兩種解釋:
- 將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不適合刊登需要貼程式碼的文章,樣式實在太少了。