0%

JS特訓-箭頭函式 (arrow function) 的 this 和你想的不一樣(上)

前言

在箭頭函數中,this 指稱的對象在所定義時就固定了,而不會隨著使用時的脈絡而改變

傳統函數下的 this 物件,有一個大原則:看呼叫時的物件是誰。不是看定義的語彙位置,而是根據執行當下誰擁有這段程式碼,也就是看誰呼叫的。

但在 Arrow Functions 卻不是這麼回事,幾乎可以說是完全不同的另一套運作邏輯。

Arrow Functions 的 this 判斷原則

Arrow Functions 的 this 和傳統函數的一個重大差異就是看的是語彙位置

傳統函數每次呼叫函數,都會建立一個新的函數執行環境 (Function Execution Context),然後建立一個新的 this 引用物件,指向當下的呼叫者。

而 Arrow Functions 則不會有自己的 this 引用物件,呼叫 this 時,會沿用語彙環境外圍的 this

我們用具體的例子來看:

物件函式

  • 函數被定義在物件之內

傳統函數

傳統函數看的是誰呼叫,所以這個例子相對單純,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
  • 函數被定義在物件之外

傳統函數

傳統函數一樣單純看是誰呼叫,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
  • 物件的屬性物件的函式

傳統函數

根據 obj.method() 的公式,呼叫者同樣很好辨認:

var player = {
  name: 'Bob',
  f: function() {
    return this;
  },
  pet: {
    name: 'May',
    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

簡易呼叫 (Simple Call)

  • 全域環境 (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
  • 內部函數 (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 物件。