跳至主要內容
Skip to content

JS 指南 (1) —— 存取器 (Get/Set) 與響應式系統深度解析

JavaScript 的 get (Getter) 和 set (Setter) 是通往「進階 JS 開發」與「框架理解(如 Vue, React)」的重要橋樑。這篇文章將帶你從零開始,系統性地掌握這項技術,並直擊它在現代框架中的核心應用。


️ 必要的預備知識 (Prerequisites)

在開始之前,請確保你對以下觀念不陌生:

  1. 物件屬性存取:了解 obj.name (Dot notation) 和 obj['name'] 的差別。
  2. 屬性 vs 方法 (Methods):存取器的目的就是讓你「用讀取屬性的語法,去執行一個函式」。
  3. this 的指向:Getter/Setter 內部的運算高度依賴 this 來讀取實例數據。
  4. 私有變數慣例:理解 _prop (工程師約定) 或原生私有欄位 #prop (ES2022)。

一、 核心概念:攔截 (Interception)

想像你是一個守門員

  • 一般屬性:球(數據)直接飛進球門(記憶體),沒人阻攔。
  • Getter (讀取):當有人要拿球時,你先攔截,加工一下(例如把球擦亮、格式化),再交給他。
  • Setter (寫入):當有人要把球丟進來時,你先攔截,檢查一下(是不是壞球、數據是否合法),再決定要不要收進倉庫。

二、 語法實作:從基礎到進階

1. 物件字面值 (Object Literal)

javascript
const user = {
  firstName: "John",
  lastName: "Doe",

  // Getter:加工後的屬性
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  // Setter:接受一個參數並反向影響內部狀態
  set fullName(value) {
    const parts = value.split(" ");
    this.firstName = parts[0] || "";
    this.lastName = parts[1] || "";
  },
};

console.log(user.fullName); // John Doe (觸發 get)
user.fullName = "Linyi Xiu"; // (觸發 set)

2. 在類別 (Class) 中使用

這是現代前端(如 TypeScript, Angular, Lit)最常見的寫法。

javascript
class Temperature {
  constructor(celsius) {
    this._c = celsius; // 真正的倉庫
  }

  get fahrenheit() {
    return (this._c * 9) / 5 + 32;
  }

  set fahrenheit(val) {
    this._c = ((val - 32) * 5) / 9;
  }
}

三、 避開最大的地雷:無窮遞迴 (Stack Overflow)

初學者最容易犯的錯誤:在 Getter 裡讀取自己,或在 Setter 裡設定自己。

javascript
// ❌ 錯誤示範
const fail = {
  get age() {
    return this.age;
  }, // 呼叫 age -> 觸發 get -> 呼叫 age... 崩潰!
};

正解:Backing Field (後備欄位) 你需要一個「倉庫」來真正存資料。通常我們會用一個不同名稱的變數(如加底線 _)來存儲內部狀態。


四、 底層與框架:Vue 2 的 Object.defineProperty

既然我們已經學會了基礎,現在來看看框架是怎麼「玩」這個特性的。

1. 響應式的起點

在 Vue 2 中,每當你將資料傳入 data(),Vue 會掃描這些物件並使用 Object.defineProperty 將它們轉化為存取器。

javascript
function reactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`[Vue 2] 收集依賴: ${key}`);
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      console.log(`[Vue 2] 觸發更新: ${key}`);
      val = newVal;
      // 在這裡通知 Watcher 重新渲染畫面
    },
  });
}

> **Vue 2 的遺憾**:它必須預先知道 Key。這就是為什麼動態新增屬性(`this.newObj.a = 1`)無法觸發更新的原因。


五、 進階演進:Vue 3 與 Proxy 的全能攔截

Vue 3 改用了 Proxy,它不再只是單點攔截,而是「全面代理」。

1. Proxy 與 Reflect 的完美搭配

javascript
const agent = new Proxy(raw, {
  get(target, key, receiver) {
    // 使用 Reflect 確保原型鏈繼承中的 this 指向正確
    const res = Reflect.get(target, key, receiver);
    track(target, key); // 依賴收集
    return res;
  },
  set(target, key, value, receiver) {
    const success = Reflect.set(target, key, value, receiver);
    trigger(target, key); // 觸發更新
    return success;
  },
});

2. 為什麼 Vue 3 使用 Proxy?

  • 自動偵測:不再需要 $set,動態屬性與陣列修改都能偵測。
  • 效能優勢:它是「按需」偵測。只有在你讀取深層屬性時,它才臨時生成 Proxy(Lazy Proxy)。

六、 四大實戰場景

  1. 數據驗證 (Validation):防止血量 (HP) 變成負數,或年齡出現小數。
  2. 計算屬性 (Computed Properties):實現連動更新,不浪費記憶體存重複數據。
  3. 封裝與相容性:重構 API 時,透過存取器讓舊程式碼不需修改即可運作。
  4. 監控與日誌:在屬性被讀取或修改時,自動發送埋點(Analytics)或 Log。

️ 進階挑戰:銀行帳戶實作

試著寫一個 BankAccount 類別:

  • 擁有私有的 _balance
  • 透過 get balance 讀取餘額(顯示時自動加上 "$" 符號)。
  • 透過 set balance 存款(禁止存入負數,並在變更時 console.log 通知)。

返回專題首頁