延續上個主題,上回我們提到了 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>

JavaScript

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

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

像上面這樣,我們就可以透過 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);

像這種時候,最簡單的解法就是用另一個變數來對 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);

上面這個例子透過 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);

如果用 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);

像上面這樣,在 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

如果你從上篇就一路看到這裡的話,相信你一定知道為什麼 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);

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

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

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

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

new Vue({
  data: {
    num: 0
  },
  methods: {
    getNum_Wrong: () => {
      // wrong!
      return this.num;
    },
    getNum: function() {
      // right!
      return this.num;
    }
  }
});

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

.call() 與 .apply()

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

假設今天有個 function 長這樣:

function func( ){
  // do something
}

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

func( );

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

func.call( );
func.apply( );

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

基本上 .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, ... ]);

.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"

可以看到,同樣是呼叫 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 的了解。