在前篇文章中我簡略的介紹了執行環境 (execution context)的概念,其中也提到了不少次 this這位神秘人物。在學習javascript的路上我曾因為它吃了不少虧,對寫習慣C或是Java等靜態語言的programmer來說它也算是個神祕的存在,因此我想在過年連假中寫這篇文章分享我對this的認知,若有寫錯的地方歡迎大家留言幫助我釐清觀念。
在開始之前我們先看下面這段code吧,看完你直覺反應console會印出什麼東西呢?
實際執行後會印出這樣:
I‘m Scot
my balance is undefined
跟你原先想的一樣嗎?如果是早已知道正確答案的人想必是踩過好幾次地雷的老手,看見它時心裡自然會產生一條警戒線。但如果你看到結果時大吃一驚也沒關係,下面內容希望可以幫助你釐清this有關的模糊地帶。
首先我們透過上面的例子可以觀察到第一個this是person物件沒錯,這裡我們就可以觀察到第一個結論:
由物件執行function時, 該function的this為該物件
以下段落討論到的rule並不考慮bind, apply, call等會改變this指標的function,本文最後會介紹這些function的使用方法與情境。
上篇文章我們有提到function被執行時JS engine會產生一個執行環境,其中包含了global object, this指標, outer environment指標以及memory space for variables and functions。在這個例子裡introduce是由person物件執行並創造新的執行環境,其中this指標就被指向person物件。
了解這個rule後我們可以把這段code改寫成下面的例子,順利拿到Scot的1000000。這個例子中getBalance藉由introduce()執行環境中的this指標(也就是person物件)執行,所以可以順利拿到person物件中的balance property。
再來我們來探討原本遇到的問題。為何一開始執行getBalance 裡面的this.name會是undefined呢?這個this又是誰?遇到不熟悉的角色我們可以console.log出來看看,這時你會發現這位this其實是global object,我們可以得出第二個觀察到的結論了。
不透過物件呼叫function時的this為global object
如果不想用將getBalance拉到property的方式解決這個問題該怎麼做呢?有些比較有歷史的code會有下面這種寫法,藉由將this暫存到一個變數解決this變化性的問題。
從上面的兩個結論我們似乎可以推導出一個假設:
this是根據執行function的方式而決定,而非定義function時就決定
此假設並不包含arrow function,本文後面會提到arrow function與this指標的關係
我們可以來驗證這個假設,先看下面這個例子:
與一開始的範例不同的是先'將person.introduce放入func變數內,並不透過person物件執行func,如果將introduce裡的this或self從console印出來會發現它們是global object。我們可以觀察到同樣是在person property的function,根據不同的使用方式會在執行環境裡得到不一樣的this,這個例子剛好驗證了我們得到的第二個結論。
上述的例子帶大家初步了解this的基本用法與情境,接下來我們看一些比較特殊的例子。看看下面範例你覺得console會印出什麼?
執行後會印出:
I’m Scot
my balance is 1000000
在這裡或許又有一部份的人mind blown,我相信有不少接觸javascript不久的人以為arrow function跟常用的Function Declarations或Function Expressions定義出來的function沒有差別,但這個例子可以從this觀察到其實有些許不同。有興趣深入了解的人可以參考這篇文章。簡單來說arrow function並沒有自己的this指標,而且arrow function使用this時會從定義位置向外尋找。上面的例子getBalance是在introduce 裡定義的,因此getBalance裡使用的this指標實際上是introduce執行環境的this指標,也就是person物件。如果我們將getBalance移至外部:
此時getBalance是定義在全域執行環境,它使用的this就會變成global object。從這個例子可以看到arrow function使用的this指標是根據定義位置取得,不是執行位置,需要再三注意!
bind(), call(), apply()
上面的例子裡this指標都是被動的由JS Engine指派,是不是覺得判斷this指標非常麻煩呢?我們其實可以使用bind(), call(), apply()這三個特殊的function指定this指標。我們先來看bind()的使用範例:
bind()執行後會回傳一個function, 並將該function的this從參數帶入,因此func()的this指標已經在定義時被指定成person物件了。call()與bind()類似,只不過他不會回傳function而是直接以指定的this指標執行原本的function,以下是使用範例:
bind()與call()除了將this指標透過參數指定外,原function使用的參數也可以透過參數傳遞。介面如下:
*fun*.bind(*thisArg*[, *arg1*[, *arg2*[, ...]]])
*fun*.call(*thisArg*[, *arg1*[, *arg2*[, ...]]])
下面是使用call()傳遞原function參數的範例:
apply()與call()又更類似了,不同的地方在於原function參數傳遞的方式,apply()是使用陣列傳遞參數:
fun.apply(thisArg, [argsArray])
以下是使用情境:
小結
this指標在我學習Javascript的旅程上曾經帶給我很長時間的苦惱,這篇文章利用大量的例子來敘述我對this的理解,除了自我複習外也希望可以幫忙對this的了解有模糊地帶的朋友。
剛接觸Javascript的時候覺得怎麼會有這麼特立獨行的語言,許多地方硬是要跟靜態語言不一樣,不過在了解它的眉眉角角後我覺得其實學JS很有趣。希望大家也可以用玩樂的心來學習JS這門藝術。
Reference
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/this
https://wcc723.github.io/javascript/2017/12/12/javascript-this/
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/Arrow_functions
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Function/call
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply