文章中英模式
布魯斯前端JS面試題目 - JavaScript this 關鍵字深入解析
深入講解 JavaScript 中 this 關鍵字的運作原理、綁定規則、箭頭函式特性,以及 call、apply、bind 方法的使用場景與最佳實踐。
文章中英模式
懶得看文章?那就來看影片吧
this 關鍵字基本原則
JavaScript 中的 this 是一個特殊關鍵字,指向函式執行時的「上下文物件」。理解 this 的核心原則是:誰調用函式,this 就指向誰。
this 的五種綁定規則
- •
預設綁定
:直接調用函式時,this 指向全域物件(非嚴格模式)或 undefined(嚴格模式) - •
隱含綁定
:作為物件方法調用時,this 指向調用該方法的物件 - •
顯式綁定
:使用 call()、apply()、bind() 明確指定 this 的值 - •
new 綁定
:使用 new 關鍵字調用函式時,this 指向新建立的物件實例 - •
箭頭函式綁定
:箭頭函式沒有自己的 this,使用外層作用域的 this
this 的不同綁定方式詳解
1. 預設綁定(Default Binding)
當函式被直接調用時,this 預設指向全域物件(瀏覽器中為 window,Node.js 中為 global)。在嚴格模式 ('use strict') 下,預設綁定的 this 為 undefined。
function printThis() {
console.log(this);
}
printThis(); // window 或 global
// 嚴格模式
function strictFunction() {
'use strict';
console.log(this);
}
strictFunction(); // undefined
2. 隱含綁定(Implicit Binding)
當函式作為物件的方法被調用時,this 指向該物件。這是最常見的 this 綁定方式。
const user = {
name: 'Bruce',
age: 30,
greet() {
console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
},
profile: {
title: 'Developer',
showTitle() {
console.log(`My title is ${this.title}`);
}
}
};
user.greet(); // "Hello, I'm Bruce and I'm 30 years old" (this 指向 user)
user.profile.showTitle(); // "My title is Developer" (this 指向 user.profile)
// 注意:隱含綁定可能丟失
const greetFn = user.greet;
greetFn(); // "Hello, I'm undefined and I'm undefined years old" (this 指向全域物件)
3. 顯式綁定(Explicit Binding)
使用 call()、apply()、bind() 明確指定函式執行時的 this 值,不論函式原本如何定義。
function introduce(hobby1, hobby2) {
console.log(`I'm ${this.name}, I like ${hobby1} and ${hobby2}`);
}
const person = { name: 'Bruce' };
// call: 參數逐一傳入
introduce.call(person, 'coding', 'reading');
// "I'm Bruce, I like coding and reading"
// apply: 參數以陣列形式傳入
introduce.apply(person, ['coding', 'reading']);
// "I'm Bruce, I like coding and reading"
// bind: 返回一個新函式,不立即調用
const boundFn = introduce.bind(person, 'coding');
boundFn('reading'); // 可以稍後傳入剩餘參數
// "I'm Bruce, I like coding and reading"
4. new 綁定(Constructor Binding)
使用 new 關鍵字調用函式時,會自動執行以下步驟,其中包含將 this 綁定到新建立的物件:
- •創建一個新的空物件
- •將該物件的原型設為構造函式的 prototype 屬性
- •將函式內的 this 綁定到新建立的物件
- •執行構造函式中的代碼
- •如果函式沒有返回其他物件,則返回這個新建立的物件
function Person(name) {
this.name = name;
// this 指向新建立的物件
}
const bruce = new Person('Bruce');
console.log(bruce.name); // "Bruce"
// this 在構造函式中指向新建立的物件實例
// 即使在全域作用域或嚴格模式下,new 綁定也有效
5. 箭頭函式綁定(Arrow Function)
箭頭函式沒有自己的 this,它會捕獲定義時所在的詞法環境中的 this 值,而且這個值不會因為調用方式的改變而改變。
const obj = {
name: 'Bruce',
// 傳統函式 - this 依呼叫方式決定
regularMethod() {
console.log(this.name);
},
// 箭頭函式 - this 鎖定在定義時的環境
arrowMethod: () => {
console.log(this.name);
},
// 常見用法: 在傳統方法內使用箭頭函式
delayedGreeting() {
setTimeout(() => {
console.log(`Hello, ${this.name}`); // this 仍指向 obj
}, 1000);
}
};
obj.regularMethod(); // "Bruce" (this 指向 obj)
obj.arrowMethod(); // undefined (this 指向定義時的全域環境)
obj.delayedGreeting(); // 1秒後輸出 "Hello, Bruce"
// 注意:箭頭函式的 this 不受 call/apply/bind 影響
obj.arrowMethod.call({name: 'Not Bruce'}); // 仍然是 undefined
解決 this 問題的常見方式
1. 使用 bind 方法固定 this
在構造函式或類的方法中,可以使用 bind 將方法的 this 顯式綁定到實例上。
class Counter {
constructor() {
this.count = 0;
// 將方法的 this 綁定到實例
this.increment = this.increment.bind(this);
}
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 即使脫離物件調用也能正常工作
console.log(counter.getCount()); // 1
2. 使用箭頭函式自動綁定
箭頭函式會自動捕獲定義時的 this 值,是處理回調函式中 this 問題的好方法。
class Counter {
constructor() {
this.count = 0;
}
// 使用箭頭函式自動捕獲外層 this
increment = () => {
this.count++;
}
getCount() {
return this.count;
}
}
const counter = new Counter();
const incrementFn = counter.increment;
incrementFn(); // 箭頭函式保持 this 指向 counter 實例
console.log(counter.getCount()); // 1
3. 使用變數保存 this
在 ES6 之前,常用的方法是在外層函式中將 this 值保存到變數中,然後在內部函式中使用該變數。
function UserProfile() {
this.name = 'Bruce';
this.friends = ['Alice', 'Bob'];
// 使用變數保存 this
const self = this;
this.showFriends = function() {
this.friends.forEach(function(friend) {
console.log(self.name + ' is friends with ' + friend);
// 使用 self 而非 this,因為這裡的 this 在非嚴格模式下指向全域
});
};
}
const profile = new UserProfile();
profile.showFriends();
// "Bruce is friends with Alice"
// "Bruce is friends with Bob"
🔥 常見面試題目
(一)this 關鍵字的值是如何決定的?請說明其判斷順序。
判斷 this 的值有固定優先順序,從高到低依次為:
- •如果是箭頭函式,this 保持定義時外部環境的 this 值,不受其他規則影響
- •如果使用 new 調用函式,this 指向新創建的物件實例
- •如果使用 call、apply、bind 等方法,this 指向指定的物件
- •如果函式作為物件的方法調用,this 指向該物件
- •如果是普通函式調用,在非嚴格模式下 this 指向全域物件,嚴格模式下為 undefined
// 示例分析 this 綁定順序
// 1. 箭頭函式優先級最高
const arrowFn = () => { console.log(this) };
arrowFn.call({ name: 'test' }); // 全域物件,call 無法改變箭頭函式的 this
// 2. new 綁定高於顯式綁定
function Person(name) {
this.name = name;
console.log(this);
}
const boundPerson = Person.bind({ type: 'bound object' });
const person = new boundPerson('Bruce'); // { name: 'Bruce' },忽略了 bind 的物件
// 3. 顯式綁定高於隱含綁定
const obj = {
name: 'object',
display() { console.log(this.name); }
};
obj.display.call({ name: 'call object' }); // "call object",call 覆蓋了方法中的 this
// 4. 隱含綁定高於預設綁定
obj.display(); // "object",this 指向 obj
// 5. 預設綁定優先級最低
function standalone() { console.log(this); }
standalone(); // 全域物件或 undefined (嚴格模式)
(二)call、apply 和 bind 的差異與使用場景?
這三個方法都能顯式指定函式執行時的 this 值,但在使用方式和行為上有明顯差異:
方法 | 執行方式 | 參數傳遞 | 常見使用場景 |
---|---|---|---|
call | 立即執行函式 | 逐一傳入參數 | 借用其他物件方法、繼承時調用父類構造函式 |
apply | 立即執行函式 | 參數以陣列形式傳入 | 處理有大量參數的場景、數學計算(如求陣列最大值) |
bind | 返回新函式,不立即執行 | 可預設部分參數 | 事件處理函式、回調函式中保持 this 上下文 |
// call 實例:借用數組方法處理類陣列物件
function formatArgs() {
// arguments 是類陣列物件,沒有 join 方法
return Array.prototype.join.call(arguments, ' - ');
}
console.log(formatArgs('a', 'b', 'c')); // "a - b - c"
// apply 實例:求陣列最大值
const numbers = [5, 3, 8, 1, 9, 4];
const max = Math.max.apply(null, numbers); // 等同於 Math.max(5, 3, 8, 1, 9, 4)
console.log(max); // 9
// bind 實例:保存 this 上下文於事件處理函式中
class Gallery {
constructor() {
this.images = ['img1.jpg', 'img2.jpg', 'img3.jpg'];
this.currentIndex = 0;
// 使用 bind 固定 this
document.getElementById('next').addEventListener(
'click',
this.showNext.bind(this)
);
}
showNext() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
console.log('Now showing: ' + this.images[this.currentIndex]);
}
}
(三)箭頭函式與一般函式在 this 綁定上有什麼不同?
箭頭函式與一般函式在 this 綁定上有以下主要差異:
- •箭頭函式沒有自己的 this,它會繼承定義時外部環境的 this 值
- •箭頭函式的 this 綁定是固定的,無法通過 call、apply、bind 等方法改變
- •一般函式的 this 值取決於調用方式,可以通過各種方法改變
// 箭頭函式與一般函式的 this 綁定差異
const obj = {
name: 'object',
regularMethod() {
console.log('Regular:', this.name);
setTimeout(function() {
console.log('Regular in timeout:', this.name); // undefined
}, 100);
},
arrowMethod() {
console.log('Arrow:', this.name);
setTimeout(() => {
console.log('Arrow in timeout:', this.name); // 'object'
}, 100);
}
};
obj.regularMethod();
obj.arrowMethod();
// 無法改變箭頭函式的 this
const arrowFn = () => console.log(this);
arrowFn.call({ name: 'test' }); // 仍然指向全域物件
// 一般函式可以改變 this
function regularFn() { console.log(this.name); }
regularFn.call({ name: 'test' }); // 'test'
(五)判斷下列程式碼中 this 的指向:
// 題目 1:基本綁定
const obj = {
name: 'object',
method() {
console.log(this.name);
}
};
obj.method(); // 'object' (隱含綁定)
// 題目 2:方法賦值
const method = obj.method;
method(); // undefined (預設綁定)
// 題目 3:回調函式
setTimeout(obj.method, 100); // undefined (預設綁定)
// 題目 4:箭頭函式
const arrowObj = {
name: 'arrow',
method() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
arrowObj.method(); // 'arrow' (箭頭函式繼承外部 this)
// 題目 5:構造函式
function Person(name) {
this.name = name;
this.sayHi = function() {
console.log('Hi, ' + this.name);
};
}
const person = new Person('Bruce');
person.sayHi(); // 'Hi, Bruce' (new 綁定)
// 解答:
// 1. obj.method() 中的 this 指向 obj,因為是通過物件調用方法(隱含綁定)
// 2. method() 中的 this 指向全域物件(非嚴格模式)或 undefined(嚴格模式),因為是直接調用(預設綁定)
// 3. setTimeout 中的 this 指向全域物件,因為回調函式是直接調用(預設綁定)
// 4. 箭頭函式中的 this 指向 arrowObj,因為箭頭函式繼承了外部環境的 this(詞法綁定)
// 5. person.sayHi() 中的 this 指向 person 實例,因為是通過 new 創建的物件調用方法(new 綁定)