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 時,從「函式如何被呼叫」這個問題出發,套用優先序,就能穩定得到答案。