[教學] JavaScript this 的用法

July 10, 2020

分類標籤:javascript frontend interview

在 JavaScript 中,「this 是什麼」絕對是讓人頭痛難題前三名。This 和物件方法息息相關,因此這篇文章會先介紹在物件方法、物件方法中的 this 是如何被決定的,把握一個簡單原則就可以知道 this 到底是誰。另外,arrow function 的 this 也很常令人搞混,這篇文章也會一併介紹 arrow function 中的 this。

目錄

物件方法 (Object Method)

物件通常對應到真實世界的事物,例如用戶、訂單、商品等。物件通常也會有自己的「動作」,可能是由一連串的資料操作、網路請求等構成的有意義的行為,例如用戶的登入登出、訂單結帳取消、商品加入購物車等。

這些「動作」在 JavaScript 中,可以用物件方法 (object method) 來實作。

所謂的物件方法,就是物件可以呼叫的方法。

比方說,我們定義一個使用者的物件 user

const user = {
  name: 'Shubo',
};

我們可以幫 user 物件新增一個 speak() 的物件方法:

user.speak = function() {
  console.log('Hello world!');
}

這樣 user 物件就可以呼叫這個 speak() 方法:

user.speak(); // Hello world!

JavaScript 中,還有很多種寫法,可以定義物件方法。

例如,我們可以直接把現成的函式指定給物件:

function speak() {
  console.log('Hello world!');
}

user.speak = speak;
user.speak(); // Hello world!

也可以在創造物件時直接定義物件方法:

const user = {
  name: 'Shubo',
  speak: function() {
    console.log('Hello world!');
  }
}

user.speak();

更簡短的寫法,在創造物件的時候,可以用省略 function 關鍵字的方式,來定義物件方法:

const user = {
  name: 'Shubo',
  speak() {
    console.log('Hello world!');
  }
}

user.speak();

什麼是 this?

如果我們在物件方法中,要存取物件本身的資料或屬性該怎麼辦呢?

比方說,我們希望使用者呼叫 speak() 方法的時候,可以順便介紹使用者自己的名字。

這個時候 this 就派上用場啦!

在物件方法中,要存取物件本身,我們可以用關鍵字 this

this 的值就是方法的呼叫者,也就是呼叫方法的物件。

舉個例子:

const user = {
  name: 'Shubo',
  speak() {
    console.log('Hello world! My name is ' + this.name); // (1)
  }
};

user.speak(); // (2) Hello world! My name is Shubo

上面的例子我們可以看到:(1) 我們在物件的 speak() 方法中用到了 this.name,接著 (2) 我們去呼叫 user.speak()

這裏的 thisuser 物件。為什麼呢?

因為「呼叫 speak() 方法的物件」是 user 物件,所以 speak() 方法中的 this 就等於 user 物件。

this 的值是動態決定的

這裡有個重要的觀念:函式中的 this 並不是一個固定不變的值。

this 的值,是在被呼叫的當下才會決定的。this 就是呼叫這個方法的物件。

單就字面或許有點難以理解,這裏我們看一個實例方便說明。

首先我定義一個 speak() 函式:

function speak() {
  console.log('Hello world! My name is ' + this.name);
}

然後我把這個函式,指定成兩個不同物件的方法:

const john = {
  name: 'John',
};

const chris = {
  name: 'Chris',
};

john.speak = speak;
chris.speak = speak;

接著我們讓兩個物件分別呼叫 speak():

john.speak();
chris.speak();

問題來了:

這兩個物件的 speak 方法是同一個方法,那這個方法中的 this 的值會是 john 物件還是 chris 物件呢?還是 undefined

這邊各位可以思考一下會印出什麼結果。

答案是:

john.speak(); // Hello world! My name is John
chris.speak(); // Hello world! My name is Chris

第一行的 thisjohn 物件,第二行的 thischris 物件。

理由是:函式中的 this 的值是被呼叫的當下,呼叫函式的物件;

第一行呼叫 speak() 的物件是 john,第二行呼叫 speak() 的物件是 chris,所以 this 分別是 johnchris,而 this.name 分別是 "John""Chris"

沒有物件的時候,this 會是什麼?

經過前面的說明,我們了解到,將函式作為一個物件方法去呼叫時,this 就是物件本身。

但是其實我們也可以在沒有物件的情境下,直接呼叫含有 this 的函式,這是完全合法的!

舉例來說,我們定義了一個 speak() 函式,其中有用到 this

function speak() {
  console.log('Hello world! My name is ' + this.name);
}

我們可以在沒有任何物件的情況下直接呼叫它:

speak();

這種情況下,因為並沒有一個物件去呼叫這個方法,所以 this 的值會是 window,或是 strict mode 底下的 undefined

註:在瀏覽器中可以用 "use strict"; 的語法開啟 strict mode,可以讓一些不良的寫法拋出錯誤。詳情請參見 Strict Mode - MDN 的說明。

Arrow function (箭頭函式) 的 this

Arrow function (箭頭函式) 是一種特別的函式,它沒有自己的 this;它的 this 會等於被宣告當下所在環境的 this

我們舉一個例子說明比較容易理解。

下面這邊我替 user 物件定義了一個物件方法 speak(),裡面又定義了一個箭頭函式 arrow()

我會在呼叫 speak() 的時候去呼叫 arrow() 方法:

const user = {
  name: 'Shubo',
  speak() {
    const arrow = () => {
      console.log('Hello world, my name is' + this.name);
    }
    arrow();
  }
}

user.speak();

值得注意的是,arrow() 會使用到 this.name。那問題來了,對 arrow() 而言,他的 this 的值是什麼?

要回答這個問題,首先必須釐清 arrow() 所在的「環境」。

我們可以看到,因為 arrow() 箭頭函式在 speak() 方法內被宣告,所以 arrow() 的「環境」就是 speak()

因此 arrow()this 就是 speak()this,也就是 user 物件。

這裡再提供一個例子供大家思考:

const arrow = () => {
  console.log('Hello world, my name is ' + this.name);
};
arrow(); // (1)

const user = {
  name: 'Shubo',
  arrow: () => {
    console.log('Hello world, my name is ' + this.name);
  },
  speak() {
    arrow();
  },
};

user.arrow(); // (2)
user.speak(); // (3)

請問 (1), (2) 和 (3) 分別會有什麼結果呢?

答案是 Hello world, my name is

原因是 arrow 被宣告的當下,他的環境是 global context。也就是說 this 等於 window,或是 strict mode 底下的 undefined

結論:該如何判斷 this 的值?

我們可以簡單歸納出決定 this 的規則:

this 就是呼叫方法時,「點」前面的那個物件。

而箭頭函式沒有自己的 this,他的 this 由外層的環境決定。

希望看到這裡,你已經了解 JavaScript 的 this 是怎麼運作的!

Reference


Profile picture

Shubo Chao 軟體工程師,目前大多專注於前端開發