Day 18:this 關鍵字 (4)

函數執行環境下 (Function Context) (續)

7. 回呼函數 (Callback Function) 裡的 this

this 物件:視乎怎麼呼叫 Callback Function。
JavaScript 裡常會需要用到 Callback Function,將某個函數物件 A 當作參數傳進另一個函數 B,由函數 B 決定執行 A 的時機。
這時候就要注意函數 B 是如何去呼叫函數 A,否則函數 A —— 也就是 Callback Function —— 裡面的 this,很可能不是你所預期的對象。

7.1. 簡單呼叫 Callback Function (一般模式)

今天我有一個 hero 物件,存在一些屬性,例如 name,我想透過 Callback 的方式去控制 hero 物件,我只在 hero 物件裡實作一個 act(),負責執行 Callback Function。而 Callback Function 的內容由其他人提供。例如以下:
1
var name = "Hi I am Global";
2
3
function sayHi(){
4
return this.name;
5
}
6
7
var hero = {
8
name: "Hi I am a Hero",
9
act: function(cbk){
10
return cbk();
11
}
12
};
13
14
console.log( sayHi() ); // Hi I am Global
15
console.log( hero.act(sayHi) ); // Hi I am Global
Copied!
  • hero.act(sayHi) 就是一個 callback 的用法,sayHi 就是 callback function。
  • hero.act() 裡,採用簡單呼叫的方式來執行 callback function。
  • 根據昨天介紹的「 2.簡易呼叫 (Simple Call) 」,函數裡的 this 會是 Global 物件,所以 hero.act(sayHi) 回傳的結果是 Global 變數的 name

7.2. 簡單呼叫 Callback Function (嚴謹模式)

1
"use strict";
2
3
var name = "Hi I am Global";
4
5
function sayHi(){
6
return this.name;
7
}
8
9
var hero = {
10
name: "Hi I am a Hero",
11
act: function(cbk){
12
return cbk();
13
}
14
};
15
16
console.log( sayHi() ); // TypeError: Cannot read property 'name' of undefined
17
console.log( hero.act(sayHi) ); // TypeError: Cannot read property 'name' of undefined
Copied!
  • 如果在嚴謹模式下,透過簡單呼叫,函數裡 this 會是 undefined,無法執行 this.name,會發生錯誤。
但上述的範例,我想達到的效果是 hero.act(sayHi) 可以回傳物件 hero 自己的 name 值,我該怎麼做?

7.3. 用 apply() / call() 將物件本身傳入 Callback Function

如果想把物件本身帶入 Callback Function 裡的 this,就要用 apply() / call()
1
var name = "Hi I am Global";
2
3
function sayHi(){
4
return this.name;
5
}
6
7
var hero = {
8
name: "Hi I am a Hero",
9
act: function(cbk){
10
return cbk.apply(this); // 將物件本身傳入 Callback Function
11
}
12
};
13
14
console.log( sayHi() ); // Hi I am Global
15
console.log( hero.act(sayHi) ); // Hi I am a Hero
Copied!

總結 this

要判斷 this 是誰,就看是誰呼叫

this 出現的位置有可能在:
  • 全域執行環境下 (Global Context)
  • 函數執行環境下 (Function Context)
但全域執行環境下 (Global Context) 遇到的機率相對低,而且十分單純,this 就是 Global 物件本身。
比較容易發生問題的是函數執行環境 (Function Context),遇到的情境可能千變萬化,過程可能讓人很混淆。
記得一個大原則:看呼叫時的物件是誰
JavaScript 的 this 不是看定義的語彙位置,而是根據執行當下誰擁有這段程式碼,也就是看誰呼叫的
但要注意的是,簡單呼叫 (Simple Call) 的情形下,一般模式和嚴謹模式會有不同的行為。

函數執行環境下,判斷 this 的公式秘笈

所有人都知道,寫程式就像學數學一樣,不建議背誦,而是去理解背後原理 (話說回來,比樂透可能性還多的語法組合,企圖用背的也很不科學)。
但當你確定你已經理解原理之後,我不反對用一些類似口訣或公式筆記的方式來輔助記憶,幫助快速回憶。畢竟如果每次寫程式要用到時,都從頭推導,那效率是不現實的。
曾有人做過實驗,讓大學數學教授和高中生一起做同一份高中數學考券,結果高中生輕輕鬆鬆在時間內完成,數學教授卻沒有寫完。是因為數學教授的程度比高中生差嗎?
當然不是。
數學教授每一個題目都知道背後原理,給他足夠的時間推導,他每一題都能完美解答。但考試時間是有限的,對那些高中生來說,這些題目範圍是他們非常熟悉,幾乎看到題目腦海就浮現解法。這代表那些高中生不理解背後原理嗎?不,他們也是經過理解的過程學到這些題目的解法,但為了應付嚴峻的考試,他們用各種方法來加速遇到題目的解決速度,例如解題口訣、公式表。
現實中的專案開發也有同樣情境。專案開發通常有時程限制,為了減少每次回憶的時間,相信很多工程師都有自己的私藏筆記,這些筆記就是你理解的精華,用來幫助自己快速回想,或減少從頭推導的時間。
前面舉了非常多情境,整體歸納下來,大致上不出以下公式的範圍:
呼叫方式
模式
this 所指的物件
obj.method()
不限
該 obj 物件
function()
一般模式
Global 物件
function()
嚴謹模式
undefined
透過 apply() 或 call()
不限
第一個參數的物件 (若第一個參數是 null,則視同「function()」)
  • 以上指的都是未經 bind() 綁定而來的函數物件
  • 透過 bind() 產生的函數,不管呼叫方式為何,this 都指向當初 bind() 所綁定的物件。

小補充:this 不是變數

這一點我想大家都清楚,只是作為 this 介紹完整性的一個小補充。
正如最一開始所說,this 是一個關鍵字,不是變數,所以不能改變 this 的值,例如企圖這樣:
1
this = { name: "OneJar" };
Copied!

References