0%

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

前言

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

顯性函數綁定 (Explicit Function Binding)

  • Function.prototype.bind( ) 篇

傳統函數

Function.prototype.bind() 可以為一個函數建立新函數物件,新函數物件會繼承原函數的 prototype,同時任意綁定一個固定的擁有者。

以下例子中的 introIronMan()introCaptainAmerica() 雖然呼叫形式上是簡單呼叫,this 會指向自己的綁定物件,而非指到 Global 物件。

1
2
3
4
5
6
7
8
9
10
11
var getFullName = function() {
return this.firstName + " " + this.lastName;
}

var firstName = "Lin", lastName = "Bob";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName()); // "Lin Bob"
console.log(introIronMan()); // "Tony Stark"
console.log(introCaptainAmerica()); // "Steven Rogers"

Arrow Functions

就像前面一再強調,Arrow Functions 的 this 判斷看的是語彙位置,因此 Function.prototype.bind() 的 Binding 不會發生作用,同樣只會沿用外層的 this 物件。

這個例子裡,getFullName() 往外一層是 Global Context,不管在一般模式或嚴謹模式,this 都是 Global 物件:

var getFullName = () => {
    return this.firstName + " " + this.lastName;
}

var firstName = "Lin", lastName = "Bob";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());           // "Lin Bob"
console.log(introIronMan());          // "Lin Bob"
console.log(introCaptainAmerica());   // "Lin Bob"
  • Function.prototype.apply() / Function.prototype.call() 篇

傳統函數

透過 apply() / call() 執行某個函數物件,同時指定一個物件作為 this,然後回傳函數的執行結果:

var whatsThis = function() {
    return this;
};
var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var captainAmerica = { firstName: "Steven", lastName : "Rogers" };

console.log(whatsThis.apply(ironMan) === ironMan);     // true
console.log(getFullName.apply(ironMan));               // "Tony Stark"
console.log(whatsThis.apply(captainAmerica) === captainAmerica);  // true
console.log(getFullName.apply(captainAmerica));        // "Steven Rogers"

Arrow Functions

就像 Function.prototype.bind() 的 Binding 不會發生作用,apply()call() 也同樣無效。只會依照語彙位置來判定 this 物件。

這個例子裡的 getFullName()whatsThis() 往外一層都是 Global Context,因此不管在一般模式或嚴謹模式,this 都是 Global 物件,而 Global 物件裡並沒有 firstNamelastName 變數,所以印出 "undefined undefined"

var whatsThis = () => {
    return this;
};
var getFullName = () => {
    return this.firstName + " " + this.lastName;
}

var ironMan = { firstName: "Tony", lastName : "Stark" };
var captainAmerica = { firstName: "Steven", lastName : "Rogers" };

console.log(whatsThis.apply(ironMan) === window);  // true
console.log(getFullName.apply(ironMan));          // "undefined undefined"
console.log(whatsThis.apply(captainAmerica) === window); // true
console.log(getFullName.apply(captainAmerica));   // "undefined undefined"

函數作為建構子

傳統函數

將函數當作建構子,透過 new 關鍵字來產生一個物件,該物件會形成自己的環境 (Context),原本函數內的 this.xxx 變成新物件的屬性。例如以下範例:

var Hero = function(n){
    this.exp = n;
};

var h = new Hero(100);
console.log(h);         // Hero {exp: 100}
console.log(h.exp);     // 100

Arrow Functions

Arrow Function 所宣告的函數不能拿來當建構子,也不存在 this 的問題。

var Hero = (n) => {
    this.exp = n;
};

var h = new Hero(100); // TypeError: Hero is not a constructor

回呼函數 (Callback Function) 裡的 this

  • 簡單呼叫 Callback Function

傳統函數

我們會把某函數 A 當作參數傳入函數 B,函數 A 就是 Callback Function。

而傳統函數裡,Callback Function 裡的 this 是誰,視乎在函數 B 裡是怎麼呼叫函數 A。如果是最常見的「簡單呼叫」的形式,此時 this 在一般模式下就是 Global 物件,嚴謹模式則是 undefined

var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am Global

Arrow Functions I

當函數 A (Callback Function) 是傳統函數,不管函數 B 是傳統函數 (hero.act1()) 還是箭頭函數 (hero.act2()),因為 Callback Function 本身是傳統函數,裡面的 this 比照傳統函數的判斷方式,也就是看呼叫方式。

由於都是透過簡單呼叫,所以 this 在一般模式下是 Global 物件,嚴謹模式是 undefined

var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk();
  },
  act2: (cbk) => {  // arrow function
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global

Arrow Functions II

當函數 A (Callback Function) 是箭頭函數,不管函數 B 是哪一種函數,都是看 Callback Function 本身的語彙位置。

由於 sayHi() 沿用外層的 this,不管是一般模式或嚴謹模式,this 都是 Global 物件:

var name = "Hi I am Global";

var sayHi = () => {    // arrow function
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk();
  },
  act2: (cbk) => {    // arrow function
    return cbk();
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global
  • 用 apply() / call() 將物件本身傳入 Callback Function

傳統函數

透過 apply() / call() 可以明確地控制函數裡的 this 物件是誰:

var name = "Hi I am Global";

function sayHi(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act(sayHi) );   // Hi I am a Hero

Arrow Functions I

當函數 A (Callback Function) sayHi() 是傳統函數時,受 apply() 效果影響:  

  • 當函數 B 也是傳統函數 (hero.act1()) :hero.act1() 自己的 this 看呼叫者是誰,也就是 hero,所以 apply()hero 綁定為 Callback Function 的 this,因此印出 "Hi I am a Hero"

  • 當函數 B 是 Arrow Function (hero.act2()) :hero.act2() 自己的 this 沿用外層,也就是 Global 物件 (無論一般模式或嚴謹模式);再透過 apply() 將 Global 物件綁定於 sayHi()this,因此印出 "Hi I am Global"

var name = "Hi I am Global";

var sayHi = function(){
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  },
  act2: (cbk) => {    // arrow function
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am a Hero
console.log( hero.act2(sayHi) );  // Hi I am Global

Arrow Functions II

由於 apply() / call() 的綁定效果對 Arrow Function 無效,如果函數 A (Callback Function) sayHi() 是 Arrow Function,無論函數 B 是傳統函數 (hero.act1()) 或者 Arrow Function (hero.act2()),sayHi() 裡的 this 都是沿用外層,也就是 Global 物件 (無論一般模式或嚴謹模式):

var name = "Hi I am Global";

var sayHi = () => {    // arrow function
  return this.name;
}

var hero = {
  name: "Hi I am a Hero",
  act1: function(cbk){
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  },
  act2: (cbk) => {    // arrow function
    return cbk.apply(this); // 將物件本身傳入 Callback Function
  }
};

console.log( sayHi() );           // Hi I am Global
console.log( hero.act1(sayHi) );  // Hi I am Global
console.log( hero.act2(sayHi) );  // Hi I am Global

可以注意到,同樣遇到 Callback Functions 的情境,傳統函數和箭頭函數的判斷原理可說是完全相反:

  • 傳統函數:看的是函數 B,也就是呼叫方怎麼呼叫函數 A。
  • 箭頭函數:看的是函數 A,也就是函數自身定義的語彙位置。

總結

一句話總結傳統函數和箭頭函數在 this 判斷上的差別

  • 傳統函數:看呼叫時的物件是誰。
  • 箭頭函數:看函數本身定義的語彙位置。