Jun

只要我的心還會跳,腿還能動

我就沒有理由停下前進的步伐

關於 this


JavaScript 的 this 相信已經是一個討論到爛掉的話題了,但即便如此它仍然不是那麼好懂,為了加深印象這篇會用偏向筆記的形式記錄常見的 this 情境並加以說明。

其實 this 可以說簡單也可以說難,簡單呢因為它不過是 ECMAScript 規範裡的一部分,關於 this 指向誰背後都是有完整的定義,但我相信一般人沒事不會去讀那種鬼東西,難呢因為想在任何情況下都能迅速的答出 this 正確的值必須也要瞭解 JavaScript 的作用域以及各種能改變 this 的方法與情境,所以廢話不多說,我們開始吧。


this 指向誰

記住這句話"this 指向最後調用它的那個物件"就這麼簡單,讓我們看看以下的範例:

function func() {
  console.log(this); // window
}

func(); // window.func();

在此調用 func 函數時其實是等價於 window.func 的因為:

所以當我們調用 func 函數時其實只是省略了 window 而已,實際上它是被 window 所調用的,那麼鑒於上面所說的"this 指向最後調用它的那個物件",這個範例的 this 所指向的就是 window

var obj = {
  func() {
    console.log(this); // obj
  },
};

obj.func();

相信這邊就很好懂了,最後調用 func 函數的物件是 obj 所以 this 自然是 obj

var obj = {
  func() {
    console.log(this); // obj
  },
};

window.obj.func();

這邊在最後一行做了一點小改動,但答案依然是 obj,仍然鑒於上面所說的那句話,最後調用函數的那個物件就是該函數的 this,雖然最後一行的寫法很完整,但最後調用 func 函數的物件就是 obj 不是 window,所以 this 就是指向 obj

var x = 'outside';

var obj = {
  func() {
    console.log(this.x); // undefined
  },
};

window.obj.func();

這邊加入了一個全域變數 x,然後我想在 func 函數裡取得 x,但我得到的是 undefined 為什麼呢?一樣鑒於上面所說的那句話,調用 func 函數的是 obj,意即 thisobj,然而 obj 裡並沒有定義 x 所以 this.x 的值就是 undefined

var x = 'outside';

var obj = {
  x: 'inside',

  func() {
    console.log(this.x); // outside
  },
};

var func2 = window.obj.func;

func2(); // window.func2();

這邊你可能會有疑問,為什麼 this.x 不是 inside?一樣是那句話,我們可以先只看最後一行,調用 func2 函數實則等價於 window.func2 的調用,所以 func2 函數是被 window 所調用的,this 自然指向 window,而 window.x 就是 outside ,所以說即便把 window.obj.func 賦值給了 func2 變數再做調用,this 的指向依然能驗證我們上面說的句話:"this 指向最後調用它的那個物件"。

var x = 'outside';

function func() {
  var x = 'inside';

  func2();

  function func2() {
    console.log(this.x);
  }
}

func();

如果你有朗誦我們的金句 7749 遍加上理解以上的範例我想這題肯定是小菜一碟,就交給各位自行跑出答案囉~


第二種理解方式

JavaScript ES5 裡有三種函數調用方式:

func(v1, v2);
obj.func(v1, v2);
func.call(context, v1, v2);

一般在工作情境我們都是使用前兩者居多,但其實第三種調用方式,才是函數真正的調用形式。

func(v1, v2); // 等價於
func.call(undefined, v1, v2);

obj.func(v1, v2); // 等價於
obj.func.call(obj, v1, v2);

接下來我們就使用第三種調用形式來理解 this 到底是誰:

func.call(context, v1, v2);

this 就是第三種調用形式裡面的 context,也就是第一個傳入的參數,如此而已,我們看看以下的範例:

function func() {
  console.log(this);
}

func(); // func.call(undefined);

這段代碼在上面的小結出現過,一個基本的函數調用,在此我們用函數原生的 call 方法調用來理解最後一行的 func(); 其實就等價於一旁的註解 func.call(undefined);

MDN 的 Function.prototype.call 裡面有提到:若此函數是在非嚴格模式下時,第一個參數為 nullundefined 將會被置換成全域變數。在此我們的全域變數就是 window

var obj = {
  func() {
    console.log(this);
  },
};

obj.func(); // obj.func.call(obj);

跟上一個範例一樣,我們在註解做了等價的代碼轉換示例,傳入的第一個參數 contextobj 所以 obj 就會成為該函數的 this

其實第二種理解方式跟第一種基本一樣,還是脫離不開"this 指向最後調用它的那個物件"這句話,只不過算是用另一個角度,透過函數原生的 call 方法來加深觀念罷了,以上只是為了理解用,相信你若聽過 call 大概率也知道 applybind,多數情況會使用到它們都是為了改變 this 的指向,所以:


call、apply、bind

我們可以透過函數原生的 callapplybind 這三個方法來改變 this 的值,但這三個函數稍有不同,不過我們先來看看範例:

'use strict';

function func(v1, v2) {
  console.log(this, v1, v2);
}

func('v1', 'v2'); // undefined "v1" "v2"
func.call(undefined, 'v1', 'v2'); // undefined "v1" "v2"
func.apply(undefined, ['v1', 'v2']); // undefined "v1" "v2"

這個範例就帶到上述重要的兩個觀念:

  1. 在嚴格模式下這樣呼叫 func('v1', 'v2'); 的話 thisundefined

  2. 第二種理解方式不斷提到的 func('v1', 'v2'); 等價於 func.call(undefined, 'v1', 'v2');

在這個範例都能被驗證,這樣對於 this 的思路是不是更清晰了呢~

那你可能會好奇 apply 呢?

applycall 的差別只在於傳入的參數,call 的參數可以一個一個傳 apply 則只接受一個陣列,就這樣,除此之外基本一模一樣。

var x = 'window';

var obj = {
  x: 'obj',
  func(v1, v2) {
    console.log(this.x, v1, v2);
  },
};

var obj2 = {
  x: 'obj2',
};

var func2 = obj.func.bind(obj2, 'v1', 'v2');

obj.func('v1', 'v2'); // obj v1 v2
func2(); // obj2 v1 v2

這邊示範了 bind 的用法,bindcallapply 的差別是它會回傳一個新的函數,而你傳入的參數就是該函數所綁定的 this

obj.func('v1', 'v2'); 是普通的調用方式,答案也可想而知,但若我們想要調用該函數且希望 this 指向的是 obj2 這個物件,我們就使用 bind 做綁定,傳入的參數方式與 call 相同但我們需要將這個新函數賦值給一個變數再做調用。

以上就是可以改變 this 的三種方式,但要注意的一點,無論你使用 callapplybind,你傳入的 context 如果是基本型別都會被轉成物件:

function func() {
  console.log(this);
}

func.call(1450); // Number {1450}
func.call('1450'); // String {'1450'}

在函數內部定義另一個變數存取 this

var obj = {
  func1() {
    console.log(this);
  },

  func2() {
    setTimeout(function() {
      console.log(this); // window
      this.func1(); // Uncaught TypeError: this.func1 is not a function
    }, 100);
  },
};

obj.func2();

這個範例,如果我想在 setTimeout 裡調用 func1 函數該怎麼做?因為 setTimeout 裡的 thiswindow 然而 window 物件裡並沒有 func1 函數所以我們會得到錯誤訊息但我們可以這麼做:

var obj = {
  func1() {
    console.log(this);
  },

  func2() {
    var that = this;

    setTimeout(function() {
      obj.func1(); // obj
      that.func1(); // obj
    }, 100);
  },
};

obj.func2();

我們可以直接用 obj.func1(); 的方式去調用也可以在 func2 函數裡的開頭就先定義另一個變數存取當前的 thisobj)後再做調用。


箭頭函數

理解箭頭函數的幾個重點:

var obj = {
  func1() {
    console.log(this);
  },

  func2() {
    console.log(this); // obj
    setTimeout(() => {
      this.func1();
    });
  },
};

obj.func2(); // obj

這個範例跟上面的一樣,但我不用定義另一個變數存取當前的 this,用箭頭函數就可取得 obj 內的 func1

箭頭函數中沒有 this,所以在 setTimeoutthis.func1(); 調用時它會查找作用域取得 this,也可以說在定義 this.func1(); 時的 this 是什麼就是什麼,我們是在 func2 內定義 this.func1();func2 內的 this 指向的是 obj,所以 this.func1(); 就等價於 obj.func1();,執行的結果 this 就是 obj


總結

this 指向誰一開始真的很難搞懂,但仍然是前端開發者必須也是必懂的主題之一,至於為什麼這麼難懂呢?因為它是 JavaScript 啊 在大部分的物件導向程式語言裡 this 就是指向 instance 本身,所以並不難理解,但在 JavaScript 裡就真的猶如在迷宮般穿梭著,不過我相信光是這句"this 指向最後調用它的那個物件"就能解掉大部分關於 this 的題目了,至於有些更偏於規範而不常出現在實際工作情境的題目可能就得去查閱 ECMAScript 才知道了。