JavaScript this 關鍵字解析|判定規則詳解

更新日期: 2024 年 11 月 1 日

核心概念

在 JavaScript 中,this 關鍵字的值取決於函數如何被呼叫,而不是根據它在哪裡定義。

這與詞法作用域(Lexical Scope)不同,後者是基於變數定義的位置。

JavaScript 的 this 關鍵字共有六個主要規則。


規則 1:誰呼叫,this 就指向誰(隱式綁定)

範例:

const hero = {                  // 定義一個物件 hero
  name: "kk",                   // hero 的 name 屬性
  age: 18,                      // hero 的 age 屬性
  attack: function() {          // 定義 hero 的 attack 方法
    console.log(this);          // 印出 this,預期指向 hero 物件
  }
}

hero.attack();                  // 呼叫 hero 的 attack 方法

說明:此段程式碼中,hero.attack()hero 物件呼叫,因此 this 指向 hero 本身。當我們呼叫物件中的方法時,this 會指向該物件。


規則 2:無人呼叫時,this 預設為全域物件(瀏覽器中為 window

無呼叫者的情境

當沒有直接呼叫者時,this 會預設為全域物件。

function hi() {                 // 定義一個函數 hi
  console.log(this);            // 印出 this,預期指向全域物件
}

hi();                           // 呼叫 hi 函數,無明確呼叫者

說明:此段程式碼中,函數 hi 被直接呼叫,而沒有特定的呼叫物件,因此 this 指向全域物件。

巢狀函數中的情境

另一種情境:

function hi() {                 // 定義一個函數 hi
  function hey() {              // 定義巢狀函數 hey
    console.log(this);          // 印出 this,預期指向全域物件
  }
  hey();                        // 呼叫 hey 函數,無明確呼叫者
}

hi();                           // 呼叫 hi 函數

說明:在此段程式碼中,heyhi 函數中的巢狀函數,且 hey 被無上下文直接呼叫,因此 this 預設為全域物件。

變數指派並無呼叫者

或是:

function hi() {                 // 定義函數 hi
  function hey() {              // 定義巢狀函數 hey
    console.log(this);          // 印出 this,預期指向全域物件
  }
  return hey;                   // 回傳函數 hey
}

const c = hi();                 // 呼叫 hi,並將回傳值(hey 函數)指派給 c
c();                            // 呼叫 c,即 hey 函數,無明確呼叫者

說明:這裡 hi 函數返回了內部的 hey 函數,並將其賦值給 c。當我們呼叫 c() 時,沒有明確的呼叫物件,因此 this 仍指向全域物件。

若函數被賦值並且在無特定呼叫者的情況下呼叫,this 依然是全域物件。

const hero = {                  // 定義 hero 物件
  name: "kk",                   // hero 的 name 屬性
  age: 18,                      // hero 的 age 屬性
  attack: function() {          // 定義 hero 的 attack 方法
    console.log(this);          // 印出 this,預期指向 hero
  }
}

const a = hero.attack;          // 將 hero 的 attack 方法指派給變數 a
a();                            // 呼叫 a,無明確呼叫者

說明:此段程式碼中,hero.attack 被指派給 a 並作為普通函數呼叫,這樣 this 就不再指向 hero,而是指向全域物件。


規則 3:箭頭函數不具有 this

箭頭函數不會綁定自己的 this,而是會從外層的詞法作用域中繼承 this

使用 arguments 的例子

const hi = function () {        // 定義一個普通函數 hi
  console.log(arguments);       // 印出 arguments,包含 hi 呼叫時的參數
}

hi(1, 2, 3);                    // 呼叫 hi,傳入參數 1, 2, 3

const haha = () => {            // 定義一個箭頭函數 haha
  console.log(arguments);       // 嘗試印出 arguments,箭頭函數無法獲得 arguments
}

haha(1, 2, 3);                  // 呼叫 haha,傳入參數 1, 2, 3

說明:此段程式碼展示了箭頭函數不具備 arguments 物件的特性,且無法獲取外部函數的 arguments。普通函數則可以使用 arguments

使用 this 的例子

const hero = {                  // 定義 hero 物件
  name: "kk",                   // hero 的 name 屬性
  age: 18,                      // hero 的 age 屬性
  attack: () => {               // 定義箭頭函數 attack,無法獲得自身的 this
    console.log(this);          // 印出 this,預期指向全域物件
  }
}

hero.attack();                  // 呼叫 hero 的 attack 方法

說明:在此例中,attack 方法被定義為箭頭函數,因此 this 會指向外層的詞法作用域,即全域物件,而非 hero


規則 4:使用 new 關鍵字時

當使用 new 關鍵字時,this 會指向由建構函數創建的新實例。

function hi() {                 // 定義一個建構函數 hi
  console.log(this);            // 印出 this,預期指向新創建的物件實例
}

new hi();                       // 使用 new 呼叫 hi,創建新物件

說明:此段程式碼示範了 new 關鍵字的作用。當使用 new 呼叫 hi 時,this 指向新創建的實例,而非全域物件。


規則 5:使用 call()apply()bind()(顯式綁定)

使用 call()

const cc = { name: 123 };       // 定義一個物件 cc

function hi() {                 // 定義一個函數 hi
  console.log(this);            // 印出 this,會由 call() 指定
}

hi.call(cc);                    // 使用 call 指定 this 為 cc 物件

說明call() 方法允許我們明確指定 this。此處 hi.call(cc) 指定 thiscc,即 hi 函數執行時的 thiscc

call() 傳遞參數

const cc = { name: 123 };       // 定義物件 cc

function hi(a, b, c) {          // 定義函數 hi,帶有參數
  console.log(this);            // 印出 this,會由 call() 指定
  console.log(a, b, c);         // 印出參數 a, b, c 的值
}

hi.call(cc, 1, 2, 3);           // 使用 call 指定 this 為 cc 並傳遞參數 1, 2, 3

說明:此段程式碼展示 call() 可以用於傳遞參數。這裡 hi.call(cc, 1, 2, 3)this 指定為 cc 並傳入了 1, 2, 3 作為參數。

使用 apply()

apply()call() 的唯一區別在於,apply() 的參數必須以陣列形式傳遞。

const cc = { name: 123 };       // 定義物件 cc

function hi(a, b, c) {         

 // 定義函數 hi,帶有參數
  console.log(this);            // 印出 this,會由 apply() 指定
  console.log(a, b, c);         // 印出參數 a, b, c 的值
}

const x = ["a", "b", "c"];      // 定義一個陣列 x 作為參數
hi.call(cc, ...x);              // 使用 call 展開陣列並傳遞參數
hi.apply(cc, x);                // 使用 apply 直接傳遞陣列作為參數

說明:此段程式碼展示了 apply() 使用參數陣列的方式將 this 綁定到 cc,而 call() 則展開陣列後分別傳遞。

使用 bind()

bind() 方法會回傳一個新函數,其 this 值為指定的物件。

const cc = { name: 123 };       // 定義物件 cc

function hi(a, b, c) {          // 定義函數 hi,帶有參數
  console.log(this);            // 印出 this,會由 bind() 指定
}

const new_hi = hi.bind(cc);     // 使用 bind 指定 this 為 cc 並回傳新函數
new_hi();                       // 呼叫新函數,印出指定的 this 值

說明bind() 可以創建一個新函數且永久綁定 this。此例中,new_hi 被永久綁定至 cc,即使後續獨立呼叫也不會改變。

結合偏函數應用的例子

function add(a, b, c) {         // 定義加法函數 add,帶有三個參數
  return a + b + c;             // 回傳參數相加的結果
}

const add3 = add.bind(null, 3); // 使用 bind 指定第一個參數 a 的值為 3
console.log(add3(4, 5));        // 呼叫 add3 並傳入其他兩個參數,結果為 3 + 4 + 5

說明:此段程式碼中,bind() 使我們可以指定部分參數,稱為偏函數應用。add3 創建時指定 a 為 3,後續只需提供剩餘參數。


使用 this 的好處說明

const hero = {                  // 定義一個物件 hero
  hp: 100,                      // hero 的血量(hp)屬性
  mp: 30,                       // hero 的魔力(mp)屬性
  attack: function () {         // 定義 hero 的 attack 方法
    console.log("ATTACK!!");    // 輸出 "ATTACK!!"
  },
};

const mage = {                  // 定義另一個物件 mage
  hp: 60,                       // mage 的血量(hp)屬性
  mp: 100,                      // mage 的魔力(mp)屬性
  attack: function () {         // 定義 mage 的 attack 方法
    console.log("attack~~~");   // 輸出 "attack~~~"
  },
  heal: function () {           // 定義 mage 的 heal 方法
    if (this.flag != "undead") {// 若 this.flag 不等於 "undead"
      this.hp += 30;            // 增加 hp 值 30
    } else {                    // 否則
      this.hp -= 30;            // 減少 hp 值 30
    }
  },
};

const monster = {               // 定義一個怪物物件 monster
  hp: 100,                      // monster 的血量(hp)屬性
  mp: 0,                        // monster 的魔力(mp)屬性
  flag: "undead",               // monster 的屬性標記(flag)為 "undead"
};

console.log(monster);           // 印出怪物的初始屬性
mage.heal.call(monster);        // 使用 call 讓 monster 使用 mage 的 heal 方法
console.log(monster);           // 印出使用 heal 方法後的怪物屬性

說明:此段程式碼說明了 this 的靈活性。mageheal 方法會依據 this 的屬性進行血量的增減,透過 call() 使 monster 使用該方法時,this 指向 monster,依據 flag 判斷進行 hp 增減。


規則 6:開啟嚴格模式

嚴格模式的影響

在啟用嚴格模式("use strict";)下,若函數在無上下文情況下被呼叫,this 的值為 undefined

"use strict";                   // 啟用嚴格模式

function hi() {                 // 定義函數 hi
  console.log(this);            // 印出 this,嚴格模式下為 undefined
}

hi();                           // 呼叫函數 hi

說明:此範例展示了嚴格模式中的 this 特性,未明確綁定的 this 在嚴格模式下將變為 undefined

嚴格模式的較佳使用方式

較佳的做法是僅在特定函數內使用嚴格模式,以避免無意的全域行為。

function hi() {                 // 定義函數 hi
  "use strict";                 // 嚴格模式僅限此函數
  console.log(this);            // 印出 this,嚴格模式下為 undefined
}

hi();                           // 呼叫函數 hi

說明:此段程式碼展示了在特定函數中啟用嚴格模式的做法,確保程式的作用範圍局限在該函數中。

為什麼嚴格模式使用字串表示?

使用 "use strict"; 作為字串可確保不同 JavaScript 引擎的相容性。若瀏覽器不支援嚴格模式,它會忽略此設定,而不會影響文件執行。

說明"use strict"; 的字串形式可確保向後相容性,舊版瀏覽器不支援嚴格模式時,這樣的設定會被安全忽略。


結論

了解這些規則和 this 的各種綁定方式,可以幫助您撰寫更具彈性且可預測的 JavaScript 程式碼。

Similar Posts