cover

結論先講

this 在 JavaScript 裡不是呼叫時的那個 function,而是根據呼叫方式動態決定。規則:

呼叫方式this 指向
獨立函式 fn()undefined(嚴格)/ window(非嚴格)
物件方法 obj.fn()obj
fn.call(x) / fn.apply(x)x
fn.bind(x) 後再呼叫x(永久綁定)
new Fn()新建的物件
箭頭函式繼承外層this(不是自己的)

搞懂這 6 種情境就解決 99% 的 this bug。React class 跟 callback 是兩個最常翻車的場景。


為什麼 this 會出 bug

const user = {
  name: 'A',
  greet() {
    console.log(this.name);
  }
};
 
user.greet();       // "A" ✅
const fn = user.greet;
fn();               // undefined ❌

同一個函式,呼叫方式不同 → this 不同。

看懂這個例子就 40% 理解 this 了。


四大綁定規則

規則 1:Default Binding(預設)

直接呼叫函式,沒任何前綴:

function foo() {
  console.log(this);
}
 
foo();
// 嚴格模式:undefined
// 非嚴格模式:window(瀏覽器)或 global(Node)

現代 ES modules 預設嚴格模式,所以 this 會是 undefined

規則 2:Implicit Binding(隱式)

透過物件呼叫:

const obj = {
  x: 10,
  show() { console.log(this.x); }
};
 
obj.show();  // 10,this = obj

關鍵this呼叫時的 . 前面是誰,不是函式定義在哪。

隱式丟失

const show = obj.show;
show();  // undefined, this 是 default
 
setTimeout(obj.show, 1000);  // 也是 default

把 method「拔出來」後,this 丟失。這是最常踩的坑。

規則 3:Explicit Binding(顯式)

.call().apply().bind() 強制指定:

function greet() { console.log(this.name); }
 
const user = { name: 'Alice' };
 
greet.call(user);    // "Alice"
greet.apply(user);   // "Alice"(只差多參數傳法)
 
const bound = greet.bind(user);
bound();             // "Alice",永久綁定
bound.call({ name: 'Bob' });  // 仍是 "Alice",bind 鎖死了

call vs apply vs bind

fn.call(thisArg, a, b, c);     // 立即呼叫,傳參用逗號分開
fn.apply(thisArg, [a, b, c]);  // 立即呼叫,傳參用陣列
fn.bind(thisArg, a, b);        // 不呼叫,回傳新函式,已綁 thisArg + 部分參數

口訣:Comma / Array / Bind。

規則 4:new Binding

function User(name) {
  this.name = name;
}
 
const u = new User('Alice');
console.log(u.name);  // "Alice"

new 做的事:

  1. 建立空物件
  2. this 指向新物件
  3. 執行函式
  4. 回傳新物件(除非函式明確 return 其他物件)

這是 ES6 class 底下的真實運作。


箭頭函式:沒有自己的 this

箭頭函式不遵守上面四條規則。它繼承外層的 this

const user = {
  name: 'A',
  greetArrow: () => console.log(this.name),
  greetRegular() { console.log(this.name); }
};
 
user.greetArrow();    // undefined(箭頭繼承外層,外層是 module/global)
user.greetRegular();  // "A"

為什麼發明箭頭函式

解決 callback 裡 this 丟失的老問題:

class Timer {
  constructor() {
    this.count = 0;
  }
 
  // ❌ 舊寫法
  startOld() {
    setInterval(function() {
      this.count++;  // 這個 this 不是 Timer!是 undefined/window
    }, 1000);
  }
 
  // ✅ 箭頭函式繼承外層 this
  startNew() {
    setInterval(() => {
      this.count++;  // 這個 this 是 Timer instance
    }, 1000);
  }
}

箭頭函式在 callback、map/filter/reduce 裡特別好用,不用一堆 bind(this)

箭頭函式的限制

  • 不能當 constructor(new ArrowFn() 噴錯)
  • 沒有 arguments 物件(用 rest params ...args 取代)
  • 不能用 yield(不是 generator)

實戰場景:React Class Component

箭頭函式是 class handler 的標準寫法

class Counter extends React.Component {
  state = { count: 0 };
 
  // ❌ 方法:this 會丟
  incrementOld() {
    this.setState({ count: this.state.count + 1 });
  }
 
  // ✅ 箭頭函式屬性:this 永遠是 instance
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };
 
  render() {
    return (
      <>
        <button onClick={this.incrementOld}>Broken</button>
        <button onClick={this.increment}>Works</button>
        <button onClick={this.incrementOld.bind(this)}>Manual bind</button>
        <button onClick={() => this.incrementOld()}>Inline arrow</button>
      </>
    );
  }
}

為什麼 onClick={this.incrementOld} 會壞

React 把 this.incrementOld 拿出來當 callback 傳。隱式綁定丟失,呼叫時 this 變 undefined。

四種修法比較

寫法效能可讀性
incrementOld = () => {} 箭頭屬性✅ 一次綁定
this.incrementOld.bind(this)❌ 每 render 新建函式
() => this.incrementOld() inline❌ 每 render 新建函式⚠️
Constructor 裡 this.x = this.x.bind(this)✅ 一次❌ 囉嗦

推箭頭屬性。Hooks 年代基本沒這問題了,但面試/維護老 code 都會遇到。


實戰場景:事件監聽器

class Button {
  constructor(el) {
    this.el = el;
    this.clicks = 0;
    // this.handleClick = this.handleClick.bind(this);  // ❌ 老做法
  }
 
  // ✅ 箭頭屬性
  handleClick = () => {
    this.clicks++;
  };
 
  attach() {
    this.el.addEventListener('click', this.handleClick);
  }
 
  detach() {
    this.el.removeEventListener('click', this.handleClick);  // 要同一個 reference
  }
}

關鍵attachdetach 要同一個函式 reference。如果用 inline () => ...,detach 拿不到同一個 reference,不會真的解除監聽。


Debug this 的秘訣

1. 看「呼叫時的 .

a.b.c.fn();  // this = c

this 永遠是最後一個 . 前面的那個物件(隱式綁定情況)。

2. Strict mode 救你

在 module 或 'use strict' 下,遺失 this 會直接噴錯而不是變 undefined 讓 bug 晚點爆。

3. TypeScript 型別系統

TS 4.3+ 有 this 型別,能在編譯期抓出很多 binding bug。


實戰 Checklist

  • 預設用箭頭函式(省煩惱)
  • 需要 this 動態決定(如物件方法)→ 用 regular function
  • bind / call / apply 只在特殊情境用
  • React class handler 用箭頭屬性
  • Event listener 要 detach 時,保留同一個函式 reference
  • 嚴格模式幫你抓 this 丟失
  • 迷茫時,找呼叫點的 . 前面是誰

相關文章