延續上個主題,上回我們提到了 this 是誰,是取決於 function 被呼叫的方式,這次我們繼續來談談 thisfunction 的關係,以及 function 中的 .call().apply().bind() 是如何對 this 來進行操作的。

前情提要

這系列的主題其實是節錄自去年 (2016) 我在五倍紅寶石開設的課程,講的是 「This」 在 JavaScript 這門程式語言裡所代表的各種面貌。 然而最近無論是社群還是課堂教學,發現仍有不少剛入門的朋友對 JavaScript 的 This 代表的意義不太熟悉,那麼我想整理出這幾篇文章也許可以釐清你對 This 的誤解,反正資料也都還在,不如就整理出來與大家分享順便做個紀錄。

系列文快速連結:

"THIS" or "THAT" ?

相信大部分朋友第一次接觸 this 的時候,應該都是在處理「事件」(event) 的綁定吧?

當我們要取得觸發事件的元素時,如 click , 這時候我們就會透過 this 來取得:

HTML

<button id="btn">按我</button>
  • 1

JavaScript

var el = document.getElementById("btn");

el.addEventListener("click", function(event) {
  console.log( this.textContent );
}, false);
  • 1
  • 2
  • 3
  • 4
  • 5

像上面這樣,我們就可以透過 this.textContent 來取得觸發 click 事件的 <button> 裡面的文字。

雖然我們可以在執行事件的 callback function 裡面透過 this 來取得觸發事件的元素,但偶爾也會遇到像這樣的例子:

// ajax function
var $ajax = function(url, callback) {
  var request = new XMLHttpRequest();
  request.open('GET', url, true);

  request.onload = function() {
    if (request.status >= 200 && request.status < 400) {
      var data = JSON.parse(request.responseText);
      if (typeof(callback) === 'function') {
        callback.call(null, data);
      }
    }
  };

  request.send();
};

// button & click event.
var el = document.getElementById("btn");

// 按下按鈕後執行 ajax,但在 callback function 的 this 卻不是你想像中的那樣
el.addEventListener("click", function(event) {
  console.log( this.textContent );

  $ajax('[URL]', function(res) {
    // this.textContent => undefined
    console.log(this.textContent, res);
  });
}, false);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

像這種時候,最簡單的解法就是用另一個變數來對 this 做參考,像這樣:

el.addEventListener("click", function(event) {
  // 透過 that 參考
  var that = this;
  console.log( this.textContent );

  $ajax('[URL]', function(res) {
    // this.textContent => undefined
    console.log(that.textContent, res);
  });

}, false);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面這個例子透過 that 這個變數來指向原本的 this,於是就可以在 callback function 裡的 scope 取得原本的 this.textContent 了。

強制指定 this 的方式

透過另一個變數來暫存 this 的方式雖然方便,那麼有沒有其他方式可以取得原本 this 的內容呢?

首先延續上一個範例,我們先看 bind()。 在上個範例中,我們用 that 這個變數來替代 this,以便取得觸發 click 事件的 el

el.addEventListener("click", function(event) {
  // 透過 that 參考
  var that = this;
  console.log( this.textContent );

  $ajax('[URL]', function(res) {
    // this.textContent => undefined
    console.log(that.textContent, res);
  });

}, false);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如果用 bind() 改寫的話,可以像這樣:

el.addEventListener("click", function(event) {
  console.log( this.textContent );

  // 透過 bind(this) 來強制指定該 function scope 的 this
  $ajax('[URL]', function(res) {
    console.log(this.textContent, res);
  }.bind(this));

}, false);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

像上面這樣,在 function 後面加上 .bind(this) 就可以強制將 ( ) 內的物件帶入至 callback function 內, 於是 callback function 裡的 this 就會強制被指定成先前在 bind( ) 裡面的內容了。

這裡有另一個簡單的比較:

var obj = {
  x: 123
};

var func = function () {
  console.log(this.x);
};

func();            // undefined
func.bind(obj)();  // 123
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果你從上篇就一路看到這裡的話,相信你一定知道為什麼 func() 的執行結果是 undefined 了。 那麼加上了 bind 之後的 func.bind(obj)() 執行的結果,會替我們將 functhis 暫時指向我們所設定的 obj。 於是, console.log(this.x) 的結果自然就是 obj.x 也就是 123 了。

這裡你可以想像成把某個 function 在執行的時候,「暫時」把它掛在某個物件下,以便透過 this 去取得該物件的 Context。

實務上除了 ajax 的 callback function 以外,另外像是 setTimeoutsetInterval 這類的 function,也滿常見需要特別處理 this 的場景。

箭頭函數與 this

值得一提的是,從 ES6 開始新增了一種叫做 「箭頭函數表示式」 (Arrow Function expression) 的函數表達式, 而箭頭函數有兩個重要的特性:更短的函數寫法this 變數強制綁定

像這樣,我們可以直接在 callback function 中取用 this.textContent

el.addEventListener("click", function(event) {
  console.log( this.textContent );

  // 箭頭函數隱含強制指定 this 至 callback function 中
  $ajax('[URL]', res => {
    console.log(this.textContent, res);
  });

}, false);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但要注意的是,無論是使用 'use strict' 或是再加上 .bind(xxx) 都無法改變 this 的內容,也不能作為建構子 (constructor)來使用。

另外,也有一些場景適不適合用箭頭函數的,像是剛剛提到的事件綁定:

el.addEventListener("click", (event) => {
  // 小心這裡的 this 變成了 「window」而不是 「el」!
  console.log( this );
}, false);
  • 1
  • 2
  • 3
  • 4

或是像 VueJS 中的 methodscomputed 會需要透過 this 來取得實體的情況等:

new Vue({
  data: {
    num: 0
  },
  methods: {
    getNum_Wrong: () => {
      // wrong!
      return this.num;
    },
    getNum: function() {
      // right!
      return this.num;
    }
  }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

箭頭函數方便歸方便,若是你的 function 內會有需要用到 this 的情況時, 就需要特別小心你的 this 是不是在不知不覺中換了人來當。

.call() 與 .apply()

既然講到了強制指定 this 的方式,看完了 bind()、「箭頭函數」,接下來就不能不講到 call()apply()

假設今天有個 function 長這樣:

function func( ){
  // do something
}
  • 1
  • 2
  • 3

那麼通常我們會直接這樣來呼叫它:

func( );
  • 1

當然你也可以用 .call() 或是 .apply() 來呼叫它:

func.call( );
func.apply( );
  • 1
  • 2

你可能會覺得奇怪,看起來沒什麼不同對吧,還要多打幾個字豈不是自找麻煩。 但如果遇上了需要帶參數的時候,就又顯得有些不同。

基本上 .call() 或是 .apply() 都是去執行這個 function ,並將這個 function 的 context 替換成第一個參數帶入的物件,換句話說,就是強制指定某個物件作為該 function 的 this。

.call().apply() 的作用完全一樣,差別只在傳入參數的方式有所不同:

function func( arg1, arg2, ... ){
  // do something
}

func.call( context, arg1, arg2, ... );
func.apply( context, [ arg1, arg2, ... ]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

.call() 傳入參數的方式是由「逗點」隔開,而 .apply() 則是傳入整個陣列作為參數。

var person = {
  name: "Kuro",
  hello: function(thing) {
    console.log(this.name + " says hello " + thing);
  }
}

var person2 = {
  name: "Jack"
};

person.hello("world");                  // "Kuro says hello world"
person.hello.call(person, "world");     // "Kuro says hello world"
person.hello.apply(person, ["world"]);  // "Kuro says hello world"

person.hello.call(person2, "world");    // "Jack says hello world"
person.hello.apply(person2, ["world"]); // "Jack says hello world"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到,同樣是呼叫 person.hello() 但是這裡可以透過 .call() 或是 .apply() 去指定當下的 this 是誰, 而差別只在傳入參數的方式有所不同。

bind, call, apply 的差異

bind() 讓這個 function 在呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 this。 尤其常用在像是 callback function 這種類型的場景,可以想像成是先綁定 this,然後讓 function 在需要時才被呼叫的類型。

.call().apply()則是使用在 context 較常變動的場景,依照呼叫時的需要帶入不同的物件作為該 function 的 this。 在呼叫的當下就立即執行。


在這次的篇幅中,我們簡單介紹了 .bind().call().apply() 以及「箭頭函數」是如何對 this 的操作, 那麼在下一篇文章當中,我們再來繼續探討「this 與前後文本 (context) 綁定的基本原則」,提供各位判斷「如何決定 this 是誰的順序」, 同時也透過例題與練習來讓各位驗證對於 JavaScript this 的了解。