Day 15:this 關鍵字 (1)

今天驟聞武俠大師金庸查先生逝世的消息,身為一位超過二十年的金迷,實在難以表達心中的難過。在此偷渡對一代文學大師的懷念,聊表追思,紀念這個對華文世界影響至深之偉人殞落的日子:金庸武俠,永垂不朽,緬懷再三,一路好走。—— 2018.10.30

this 是一個特殊的關鍵字,代表著一個物件,在很多程式語言都可以看到這個設計。由於不同程式語言有各自的特性, this 的運作方式也不盡相同。

那在 JavaScript 裡,this 指什麼呢?

來看看 W3Schools 的說明:

  • In a method, this refers to the owner object.

  • Alone, this refers to the global object.

  • In a function, this refers to the global object.

  • In a function, in strict mode, this is undefined.

  • In an event, this refers to the element that received the event.

  • Methods like call(), and apply() can refer this to any object.

JavaScript 的 this 一下是物件本身,一下是 Global 物件,一下是 undefined,一下還可以是任何物件!

this 是程式語言中很重要的部分,誤判 this 所代表的物件會直接對程式的運作造成影響。所以本篇文章就來好好弄清楚 JavaScript 的 this

this 的簡單範例

照例先上一個基本款的情境範例:

var ironMan = {
    firstName: "Tony",
    lastName : "Stark",
    getFullName : function() {
        return this.firstName + " " + this.lastName;
    }
};

console.log(ironMan.getFullName()); // "Tony Stark"

這是一個典型的 this 應用例子,對物件進行物件導向風格的操作。上面這個例子看起來很好懂,那 JavaScript 的 this 到底複雜在哪?

JavaScript 的 this 很複雜?

JavaC# 等基於類別的物件導向語言 (Class-based Object-oriented Languages),由於語法規範極為嚴謹,大多數情境可以從語彙範圍 (Lexical Scope) —— 也就是看語法上定義在哪個地方來判斷。

但 JavaScript 的 this 關鍵字運作與其他語言不同。具體來說,JavaScript 的 this 不是看定義的語彙位置,而是根據執行當下誰擁有這段程式碼

W3Schools: this has different values depending on where it is used. The JavaScript this keyword refers to the object it belongs to.

換句話說,不是看該屬性或函式被定義在哪個物件內,看的是執行當下被誰呼叫

JavaScript 的語彙定義上沒有像類別那樣完整明確的物件界線,加上語法結構設計上很寬鬆,衍生出各種複雜情境。

此外,不同情境下,this 運作的機制也不同,例如:

  • 全域執行環境 (Global Context) 下和函數執行環境 (Function Context) 下不同。

  • 在嚴謹模式 (Strict Mode) 與一般模式下也可能有所不同。

理論描述太抽象,接下來會試圖用各種實際的例子來探討 this 的運作。

全域執行環境下 (Global Context)

全域環境下比較單純。

MDN: In the global execution context (outside of any function), this refers to the global object whether in strict mode or not.

  • this 在所有函式以外的全域執行環境下,會被當作全域物件,無論是否處於嚴謹模式

    • HTML 中就是 window 物件。

    • Node.js 中就是 global 物件。

例如在全域執行環境下直接印出 this 物件:

一般模式:

console.log(this); // window object

嚴謹模式:

"use strict";

console.log(this); // window object

函數執行環境下 (Function Context)

函數環境下可能遇到的情境就複雜了。

MDN: Inside a function, the value of this depends on how the function is called.

在函數內的 this 值取決於該函數如何被呼叫。

就先記著一個大原則:看是誰呼叫的,然後用這個原則來看各種情境下 this 運作的例子。

1. 物件函式 (As an Object Method)

this 物件:呼叫者本身。

這應該是最常見、也相對好理解的情境,也就是 Object Method Binding。

在物件函式的用法,嚴謹模式或一般模式都是一樣的執行結果。

但同樣是物件函式的用法,也有很多種語法情境。

1.1. 函數被定義在物件之內

下面例子中 player 物件有 2 個函式,其中一個直接在函數內回傳 this,藉此來觀察當下的 this 值:

var player = {
  name: 'OneJar',
  getName: function() {
    return this.name;
  },
  whatsThis: function() {
    return this;
  },
};

console.log(player.getName());      // "OneJar"
console.log(player.whatsThis());    // {name: "OneJar", getName: ƒ, whatsThis: ƒ}    # `player` object
  • 執行的函數是 player.whatsThisplayer.getName 所指向的函數。

  • 呼叫者是 player

  • 因此函數內的 this 指的是 player

這個例子理解上應該沒什麼困難,很單純的呼叫物件函式用法,加上函數定義就在物件內,判斷上很簡單。

如果函數定義不在物件內呢?

1.2. 借用函數 (函數被定義在物件之外)

函數不是物件本身自己定義,而是指向別人的函數,就像跟別人借用函數一樣。

那在函數裡的 this,會是物件本身,還是別人?

是不是開始有趣了。

來看下面的例子:

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

var player = { name: 'OneJar' };
player.f1 = getName;
player.f2 = whatsThis;

console.log(player.f1());     // "OneJar"
console.log(player.f2());     // {name: "OneJar", getName: ƒ, whatsThis: ƒ}    # `player` object
  • getName()whatsThis() 函數都不是定義在 player 內。

  • 但執行當下,player 是呼叫者,被視為那段程式碼的擁有者,因此 this 仍是 player

函數是否為物件本身的語彙構件無所謂,誰是最直接的呼叫才是最重要的

1.3. 物件的屬性物件的函式

物件內的屬性可以是另一個物件,另一個物件也可以有自己的函式,那這時候的 this 是誰?

例如以下例子:

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

var player = {
  name: 'OneJar',
  f: getName,
  pet: {
    name: 'Totoro',
    f: getName,
  }
};

console.log(player.f());      // "OneJar"
console.log(player.pet.f());  // "Totoro"
  • player 物件擁有另一個物件 pet,而 playerpet 都借用 getName()

  • player.f() 的呼叫者是 player

  • player.pet.f() 的呼叫者是 player.pet

這裡的原則沒有變,一樣看誰是呼叫者,誰就是 this

只是不要混淆,player.pet.f() 的呼叫者是 player.pet 而不是 player

References

Last updated