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 函數
說明:在此段程式碼中,hey
是 hi
函數中的巢狀函數,且 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)
指定 this
為 cc
,即 hi
函數執行時的 this
為 cc
。
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
的靈活性。mage
的 heal
方法會依據 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 程式碼。