魯斯前端布魯斯前端

文章中英模式

布魯斯前端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 綁定)