JS 指南 (1) —— 存取器 (Get/Set) 與響應式系統深度解析
JavaScript 的 get (Getter) 和 set (Setter) 是通往「進階 JS 開發」與「框架理解(如 Vue, React)」的重要橋樑。這篇文章將帶你從零開始,系統性地掌握這項技術,並直擊它在現代框架中的核心應用。
️ 必要的預備知識 (Prerequisites)
在開始之前,請確保你對以下觀念不陌生:
- 物件屬性存取:了解
obj.name(Dot notation) 和obj['name']的差別。 - 屬性 vs 方法 (Methods):存取器的目的就是讓你「用讀取屬性的語法,去執行一個函式」。
this的指向:Getter/Setter 內部的運算高度依賴this來讀取實例數據。- 私有變數慣例:理解
_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)。
六、 四大實戰場景
- 數據驗證 (Validation):防止血量 (HP) 變成負數,或年齡出現小數。
- 計算屬性 (Computed Properties):實現連動更新,不浪費記憶體存重複數據。
- 封裝與相容性:重構 API 時,透過存取器讓舊程式碼不需修改即可運作。
- 監控與日誌:在屬性被讀取或修改時,自動發送埋點(Analytics)或 Log。
️ 進階挑戰:銀行帳戶實作
試著寫一個 BankAccount 類別:
- 擁有私有的
_balance。 - 透過
get balance讀取餘額(顯示時自動加上 "$" 符號)。 - 透過
set balance存款(禁止存入負數,並在變更時console.log通知)。