this 是 JavaScript 最容易誤解的關鍵字之一。關鍵心法是:this 不是在「函式定義時」決定,而是在「函式被呼叫時」決定。同一個函式,用不同方式呼叫,this 就指向不同的物件。
理解 this 只要掌握「函式怎麼被呼叫」這一件事,就能推導出它的值。
四種呼叫方式#
| 呼叫方式 | 範例 | this 指向 |
|---|---|---|
| 一般函式呼叫 | fn() | 全域物件(嚴格模式為 undefined) |
| 方法呼叫 | obj.fn() | 點號左邊的物件 obj |
| 建構函式呼叫 | new Fn() | 新建立的實例 |
| 顯式綁定 | fn.call(ctx) / bind | 第一個參數 ctx |
1. 一般函式呼叫#
直接呼叫一個函式,this 指向全域物件(瀏覽器中是 window)。在嚴格模式下,this 會是 undefined,這能避免意外汙染全域。
function show() {
console.log(this);
}
show(); // 非嚴格模式:window;嚴格模式:undefined
最常見的陷阱:把方法「拆」出來單獨呼叫,
this就脫離了原本的物件。const user = { name: "Andrew", greet() { console.log(this.name); }, }; const fn = user.greet; fn(); // undefined(this 不再是 user)
2. 方法呼叫#
透過 物件.方法() 呼叫時,this 指向「點號左邊」的物件。重點永遠看「呼叫當下」的那個物件,而不是方法定義在哪裡。
const user = {
name: "Andrew",
greet() {
console.log(this.name);
},
};
user.greet(); // "Andrew"
3. 建構函式呼叫#
用 new 呼叫時,引擎會建立一個全新的物件,並把 this 綁定到這個新物件上,最後(在沒有顯式回傳物件時)回傳它。
function User(name) {
// this = 新建立的空物件
this.name = name;
// 隱式 return this
}
const u = new User("Andrew");
console.log(u.name); // "Andrew"
4. 顯式綁定:call / apply / bind#
這三個方法讓你「手動指定」this。
function introduce(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: "Andrew" };
// call:參數逐一傳入
introduce.call(user, "Hi", "!"); // "Hi, Andrew!"
// apply:參數用陣列傳入
introduce.apply(user, ["Hi", "!"]); // "Hi, Andrew!"
// bind:回傳一個「永久綁定 this」的新函式,不立即執行
const boundIntro = introduce.bind(user);
boundIntro("Hello", "."); // "Hello, Andrew."
| 方法 | 是否立即執行 | 參數形式 | 典型用途 |
|---|---|---|---|
call | 是 | 逐一列出 | 借用方法、立即指定 this |
apply | 是 | 陣列 | 參數已是陣列(如 Math.max) |
bind | 否 | 逐一列出 | 事件 callback、固定 this |
// apply 的經典用途:把陣列展開為參數
const nums = [3, 1, 4, 1, 5];
Math.max.apply(null, nums); // 5
// 現代等價寫法
Math.max(...nums); // 5
綁定的優先序#
當多種規則同時可能適用時,優先序由高到低為:
new 綁定 > 顯式綁定(call/apply/bind) > 方法呼叫 > 一般函式呼叫function f() {
console.log(this.val);
}
const a = { val: "a" };
const b = { val: "b" };
const bound = f.bind(a);
bound(); // "a"
bound.call(b); // 仍是 "a"(bind 的優先序高於後來的 call)
bind之後再call也無法覆蓋,這正是「永久綁定」的意義。bind一旦設定,this就固定了。
箭頭函式:沒有自己的 this#
箭頭函式不適用上述四種規則。它不綁定自己的 this,而是沿用「定義時所在的外層作用域」的 this(詞法綁定)。這也是它在 callback 中最受歡迎的原因。
const timer = {
seconds: 0,
start() {
// 箭頭函式沿用 start() 的 this(即 timer)
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
},
};
timer.start(); // 1, 2, 3...(this 正確指向 timer)
對照之下,用一般函式當 callback 就會踩雷:
start() {
setInterval(function () {
this.seconds++; // this 是 window/undefined,不是 timer
}, 1000);
}不要把物件的方法寫成箭頭函式。 因為箭頭函式的
this來自外層(通常是模組或全域),而非呼叫它的物件:const user = { name: "Andrew", greet: () => { console.log(this.name); // undefined,this 不是 user }, };
手寫一個簡化版 bind
理解 bind 最好的方式是自己實作一個:
Function.prototype.myBind = function (ctx, ...presetArgs) {
const fn = this; // 原本要綁定的函式
return function (...laterArgs) {
return fn.apply(ctx, [...presetArgs, ...laterArgs]);
};
};
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const bound = greet.myBind({ name: "Andrew" });
bound("Hi"); // "Hi, Andrew"
核心就是用閉包記住 ctx,再用 apply 在呼叫時套用。
小結#
| 規則 | 判斷依據 |
|---|---|
| 一般呼叫 | 沒有任何前綴 ➡️ 全域 / undefined |
| 方法呼叫 | 看點號左邊的物件 |
new | 指向新建立的實例 |
| call/apply/bind | 看傳入的第一個參數 |
| 箭頭函式 | 沿用定義時外層的 this |
判斷 this 時,從「函式如何被呼叫」這個問題出發,套用優先序,就能穩定得到答案。