你不可不知的 JavaScript 二三事#Day21:箭頭函數 (Arrow Functions) 的 this 和你想的不一樣 (1)
前面 Day15 ~ Day18 舉了很多例子來剖析傳統函數在各種情境下的 this
物件。
過程雖然眼花繚亂,但有一個大原則:看呼叫時的物件是誰。不是看定義的語彙位置,而是根據執行當下誰擁有這段程式碼,也就是看誰呼叫的。
但在 Arrow Functions 卻不是這麼回事,幾乎可以說是完全不同的另一套運作邏輯。
Day20 介紹箭頭函數 (Arrow Functions) 時示範了一個 this
的例子,可以發現和傳統函數的 this
運作原則大不相同。
這就是為什麼最好把 Arrow Functions 當成有別於傳統函數的新種族,在運作本質上他們有顯著的差別。
Arrow Functions 的 this
判斷原則
那在 Arrow Functions 該怎麼判斷呢?
MDN & W3Schools:
- In arrow functions, this retains the value of the enclosing lexical context's this.
- Arrow functions do not have their own this.
- In global code, it will be set to the global object.
Arrow Functions 的 this
和傳統函數的一個重大差異就是看的是語彙位置。
傳統函數每次呼叫函數,都會建立一個新的函數執行環境 (Function Execution Context),然後建立一個新的 this
引用物件,指向當下的呼叫者。
而 Arrow Functions 則不會有自己的 this
引用物件,呼叫 this
時,會沿用語彙環境外圍的 this
。
相信這樣還是很模糊,我們一樣用具體的例子來看。
下面會將前幾天探討傳統函數 this
的情境範例,換成 Arrow Functions 的狀況,並和傳統函數做比對。
傳統函數的部分由於前幾天的文章已經詳細解析過,可參考 Day15 ~ Day18 的文章,下面就不再重複細節,會很快帶過,著重在 Arrow Functions 部分的解析和對照。
1. 物件函式
1.1. 函數被定義在物件之內
傳統函數
傳統函數看的是誰呼叫,所以這個例子相對單純,player.whatsThis()
的呼叫者就是 player
:
var player = {
whatsThis: function() { // normal function
return this;
},
};
console.log( player.whatsThis() === player ); // true
Arrow Functions
whatsThis()
使用 Arrow Functions 定義,因此 whatsThis()
本身不會有自己的 this
,而是沿用外圍環境的 this
。
在這個例子裡,從 whatsThis()
再往外一層不在任何 Function Context 內,換言之就是 Global Context。在全域環境裡的 this
就是 Global 物件,在 HTML 裡就是 window
:
var player = {
whatsThis: () => { // arrow function
return this;
},
};
console.log( player.whatsThis() === window ); // true
1.2. 借用函數 (函數被定義在物件之外)
傳統函數
傳統函數一樣單純看是誰呼叫,player.f()
的呼叫者就是 player
:
var whatsThis = function() { // normal function
return this;
};
var player = {};
player.f = whatsThis;
console.log(player.f() === player); // true
Arrow Functions
雖然是透過 player.f()
呼叫,但這邊看的是語彙位置,whatsThis()
再往外一層的 this
是 Global 物件:
var whatsThis = () => { // arrow function
return this;
};
var player = {};
player.f = whatsThis;
console.log(player.f() === window); // true
1.3. 物件的屬性物件的函式
傳統函數
根據 obj.method()
的公式,呼叫者同樣很好辨認:
var player = {
name: 'OneJar',
f: function() {
return this;
},
pet: {
name: 'Totoro',
f: function() {
return this;
},
}
};
console.log(player.f() === player); // true
console.log(player.pet.f() === player.pet ); // true
Arrow Functions
雖然呼叫者不同,函數定義的層次也不太一樣,但由於 player.f()
和 player.pet.f()
語彙位置再往外一層其實都是 Global Context,所以兩個函數回傳的 this
都是 Global 物件:
var player = {
name: 'OneJar',
f: () => {
return this;
},
pet: {
name: 'Totoro',
f: () => {
return this;
},
}
};
console.log(player.f() === window); // true
console.log(player.pet.f() === window ); // true
2. 簡易呼叫 (Simple Call)
2.1. 全域環境 (Global Context) 下定義函數 & 呼叫函數
傳統函數
前面沒有指定呼叫者的狀況,傳統函數在一般模式下是 Global 物件,嚴謹模式下是 undefined
:
var whatsThis = function() {
return this;
}
console.log( whatsThis() ); // (normal mode) window / (strict mode) undefined
Arrow Functions
由於看的是語彙位置,往外一層是 Global Context,不管在一般模式或嚴謹模式,this
都是 Global 物件:
var whatsThis = () => {
return this;
}
console.log( whatsThis() ); // window
2.2. 內部函數 (Inner Functions)
傳統函數
var x = 10;
var obj = {
x: 20,
f: function(){
console.log('Output#1: ', this.x);
var foo = function(){ console.log('Output#2: ', this.x); }
foo();
}
};
obj.f();
執行結果:
Output#1: 20
Output#2: 10
- 「Output#1」時,呼叫方式是
obj.f()
,因此this
是呼叫者obj
物件,this.x
是 20。 - 「Output#2」時,呼叫方式是
foo()
,視同簡單呼叫,一般模式下this
是 Global 物件,因此this.x
是 10。
Arrow Functions I
如果只有內部函數是 Arrow Function,外部函數仍是傳統函數:
var x = 10;
var obj = {
x: 20,
f: function(){
console.log('Output#1: ', this.x);
var foo = () => { console.log('Output#2: ', this.x); } // arrow function
foo();
}
};
obj.f();
執行結果:
Output#1: 20
Output#2: 20
- 「Output#1」所在的函數仍是傳統函數,因此
this.x
不變仍是 20。 - 「Output#2」所在的函數變成 Arrow Function,沿用外層的
this
,其外層就是obj.f()
,因此this.x
也是 20。
Arrow Functions II
如果外部函數是 Arrow Function,內部函數是傳統函數:
var x = 10;
var obj = {
x: 20,
f: () => { // arrow function
console.log('Output#1: ', this.x);
var foo = function() { console.log('Output#2: ', this.x); }
foo();
}
};
obj.f();
執行結果:
Output#1: 10
Output#2: 10
- 「Output#2」根據呼叫方式是
foo()
,視同簡單呼叫,一般模式下this
是 Global 物件,因此this.x
是 10。 - 「Output#1」會沿用外層的
this
,往外找一層是 Global Context,所以this
也是 Global 物件。
Arrow Functions III
如果外部函數和內部函數都是 Arrow Function:
var x = 10;
var obj = {
x: 20,
f: () => { // arrow function
console.log('Output#1: ', this.x);
var foo = () => { console.log('Output#2: ', this.x); } // arrow function
foo();
}
};
obj.f();
執行結果:
Output#1: 10
Output#2: 10
- 「Output#1」會沿用外層的
this
,往外找一層是 Global Context,所以this
也是 Global 物件。 - 「Output#2」沿用外層的
this
,其外層是obj.f()
;而obj.f()
的this
如上面所說,經過沿用後是 Global 物件。
可以發現上面第 2 和 第 3 個情境的結果都是 Output#1 和 Output#2 等於 10。雖然他們最後呈現的結果碰巧一樣,但要注意背後的運作原理其實有所差別。