Day 21:箭頭函數 (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。雖然他們最後呈現的結果碰巧一樣,但要注意背後的運作原理其實有所差別。

References

Last updated