一次搞懂 JavaScript 的 this:簡單實用的指南
前言
在 JavaScript 的世界裡,有一個讓新手困惑、讓老手心生畏懼的主題:this
到底是什麼?
「為什麼這個函式裡的
this
是undefined
?」 「物件的方法好好的,為什麼this
突然變成了window
?」 「箭頭函式的this
跟普通函式不一樣?到底要怎麼判斷?」
如果你曾在開發中遇到這樣的問題,我想對你說:你並不孤單😿
雖然網路上已經有很多介紹 this
的文章,我可能也不會寫得比其他文章還深入,但我還是想要以我目前對 this
的理解程度,站在我的視角,來分享我是如何理解 this
的。當然,這篇文章不會「完全」解釋所有 this
的邏輯,畢竟要徹底理解它,你還得翻開 ECMAScript 規範才行。但我保證,這篇文章能夠幫助你在大多數情況下快速判斷 this
,甚至對 this 有更深一層的認識。
這篇文章適合 對 JavaScript 有一定基礎 的開發者。文章內容將會包括:
- JavaScript 的 基本語法(如物件、函式、
class
等)。 - JavaScript 的 作用域 和 閉包 是什麼(沒關係,這裡也會簡單提到)。
如果這些你還不太熟悉,建議先補充相關知識,否則讀到後面你可能會更加混亂。
為什麼 JS 的 this 這麼難懂?
其實,JavaScript 的 this
之所以讓人困惑,是因為它和其他物件導向語言的 this
有所不同:
- 它可以脫離物件被呼叫。
- 它可以被手動改變
this
的指向(例如call
、apply
和bind
)。 - 它還有箭頭函式這種特殊存在,會繼承外部作用域的
this
。
JavaScript 的 this
最大的特點在於:
this
的值並不是在「函式定義時」決定的,而是在「函式執行時」根據呼叫方式動態決定的。
這種彈性雖然賦予了 JavaScript 很大的自由度,但也讓 this
變得複雜且容易出錯。
this
的本質:物件導向的延伸
在大多數物件導向語言中,this
從來都不是什麼難懂的概念。它的存在非常單純:它代表當前實例(instance)本身,方便在類別內存取物件的屬性或方法。但在 JavaScript 裡面,好像並沒有這麼單純。
物件導向語言裡的 this
我們先來看看一個簡單的例子:
class Car {
setName(name) {
this.name = name; // this 指向當前的實例
}
getName() {
return this.name;
}
}
const myCar = new Car();
myCar.setName('Tesla');
console.log(myCar.getName()); // Tesla
在這段程式碼中,this
的存在是必需的,因為我們需要一個方式來指代當前物件的屬性或方法。
this.name = name
:表示把傳進來的name
設定到當前實例 的name
屬性上。myCar.setName('Tesla')
呼叫時,this
指向myCar
,所以this.name
實際上是myCar.name
。
這種寫法在物件導向語言中是非常直觀的,因為 this
就是物件 自己的「代名詞」。
脫離物件導向的 this
然而,在 JavaScript 中,this
並不局限於物件或類別內,它可以出現在任何地方!
- 在函式中,
this
可能是全域物件(window
或global
)。 - 在事件處理函式中,
this
指向觸發事件的元素。 - 在
setTimeout
、箭頭函式等情境下,this
的行為又不一樣了。
我們來看一個例子:
function hello() {
console.log(this);
}
hello();
你覺得這裡的 this
是什麼?
在其他語言中,這段程式碼可能根本不成立,因為 this
只有在類別或物件內才有意義。但在 JavaScript 裡,this
會根據執行環境給出一個預設值:
- 非嚴格模式:
this
指向 全域物件(瀏覽器中是window
,Node.js 是global
)。 - 嚴格模式:
this
是undefined
。
"use strict";
function hello() {
console.log(this);
}
hello(); // undefined
當 this
脫離物件,並單純存在於一般函式中時,它其實沒有什麼太大的意義,僅僅是語言機制給了一個預設值罷了。
(引用自 @淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂):
this
的指向規則
前面我們有提到,在 JavaScript 中,this
的值不是在函式定義時決定的,而是根據 「函式執行時的呼叫方式」 動態決定的,這是理解 this
的核心關鍵。在判斷 this 值時,我們需要時刻記住一個核心原則:
要看
this
,就看「誰,在哪裡呼叫了這個函式」
全域環境中的 this
我們先從最簡單的情況開始:在全域環境中 this
的值是什麼?
範例:
console.log(this);
結果:
- 瀏覽器環境:
this
指向window
物件。 - Node.js 環境:
this
指向global
物件(模組作用域則是{}
)。
一般函式中的 this
當函式不是物件方法,而是單獨呼叫時,this
的值取決於是否處於「嚴格模式」。
範例:
function hello() {
console.log(this);
}
hello(); // 非嚴格模式
結果:
- 非嚴格模式:
this
指向全域物件(window
或global
)。 - 嚴格模式:
this
是undefined
。
嚴格模式範例:
"use strict";
function hello() {
console.log(this);
}
hello(); // undefined
小結:普通函式中的 this 取決於是否嚴格模式。如果脫離物件,this 基本上沒有意義,只會回傳預設值。
物件方法中的 this
當函式作為「物件的方法」被呼叫時,this
指向呼叫該方法的物件。
範例:
const obj = {
name: 'Alice',
sayName() {
console.log(this.name);
}
};
obj.sayName();
結果:
this
指向obj
,因此輸出Alice
。
箭頭函式中的 this
箭頭函式是個特例,它不會產生自己的 this
,而是繼承自定義時的外部作用域。
範例:
const obj = {
name: 'Alice',
sayName() {
const arrowFunc = () => {
console.log(this.name);
};
arrowFunc();
}
};
obj.sayName();
結果:
this
繼承自sayName
方法中的this
,也就是obj
,所以輸出Alice
。
小結:箭頭函式的 this 是靜態的,取決於它被「定義時」所在的作用域。
setTimeout 中的 this
在 setTimeout
中,回呼函式的 this
取決於它是普通函式還是箭頭函式。
範例:
const obj = {
name: 'Alice',
sayName() {
setTimeout(function() {
console.log(this.name);
}, 1000);
}
};
obj.sayName();
結果:
- 由於回呼函式是普通函式,
this
在非嚴格模式下會指向window
,輸出undefined
。
解決方法:使用箭頭函式:
setTimeout(() => {
console.log(this.name);
}, 1000);
- 箭頭函式會繼承
sayName
中的this
,所以輸出Alice
。
事件處理中的 this
當函式用作 DOM 事件處理時,this
指向觸發事件的元素。
範例:
const button = document.createElement('button');
button.innerText = 'Click me';
button.addEventListener('click', function() {
console.log(this); // 指向觸發事件的元素
});
document.body.appendChild(button);
結果:
this
指向button
元素。
如果改成箭頭函式呢?
button.addEventListener('click', () => {
console.log(this);
});
- 箭頭函式會繼承定義時的
this
,在這裡指向全域物件(window
)。
改變 this
的方法:call
、apply
和 bind
有時候我們不滿意 JavaScript 預設的 this
指向,這時候可以透過 call
、apply
和 bind
來手動改變 this
的值。
為什麼要改變 this
?
我們先看一個常見的問題:
const obj = {
value: 42,
getValue() {
console.log(this.value);
}
};
const extracted = obj.getValue;
extracted(); // undefined
為什麼印出來的是 undefined ?看得出來問題出在哪嗎?
這是因為,當我們把 obj.getValue
提取出來後,它成為了一個獨立函式。由於這個函式不是透過物件呼叫的,this
指向了全域物件(非嚴格模式下是 window
)。
這時候我們可以使用 call
、apply
或 bind
來手動指定 this
。
call
、apply
皆回傳 function 執行結果bind
方法回傳的則是綁定 this 後的原函數
call()
方法
語法:
function.call(this, arg1, arg2..., argn)
thisArg
:指定this
的值。arg1, arg2, ...
:傳給函式的參數,依序傳遞。
範例:
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello', '!'); // Hello, Alice!
- 在這裡,
call
強制將this
指向person
,並立即執行greet
函式。
apply()
方法
語法:
function.apply(this, [arg1, arg2..., argn])
thisArg
:指定this
的值。[argsArray]
:參數以陣列的形式傳入。
範例:
greet.apply(person, ['Hi', '!']); // Hi, Alice!
- 差異點:
apply
和call
唯一的區別在於參數的傳遞方式。call
用逗號分隔,apply
則使用陣列。
bind()
方法
語法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg
:指定this
的值。arg1, arg2, ...
:可以預設一些參數(可選)。
範例:
const boundGreet = greet.bind(person, 'Hey');
boundGreet('!'); // Hey, Alice!
bind
創造一個函式物件的拷貝,這個拷貝函式的this
會永遠被綁定成thisArg
,即使後續我們再用call
或再次bind
,也無法改變一開始被綁定的this
。bind
後面傳入的參數值也會設定為拷貝函式的永久參數值,之 後執行拷貝函式時,無論怎麼給予參數都沒有用
其他應用場景
以下我們多看幾個手動改變 this
值的應用場景:
-
解決事件處理中的
this
問題const obj = {
value: 'Hello',
showValue() {
console.log(this.value);
}
};
const button = document.createElement('button');
button.innerText = 'Click me';
// 事件處理中的 this 會指向觸發事件的元素
button.addEventListener('click', obj.showValue); // undefined
// 解決 方法:用 bind 綁定 this
button.addEventListener('click', obj.showValue.bind(obj)); // Hello
document.body.appendChild(button);