JavaScript 物件(Objects)— Getter / Setter 完全指南
簡介
在 JavaScript 中,物件是最核心的資料結構之一。除了直接存取屬性外,我們常常需要在讀取或寫入屬性時執行額外的邏輯,例如驗證資料、計算衍生值或觸發副作用。Getter(取得器)與 Setter(設定器)正是為了這類需求而設計的語法糖。
使用 getter / setter 可以讓程式碼 看起來像在直接操作屬性,同時在背後完成更複雜的工作,提升封裝性與可維護性。對於前端開發者而言,這在資料綁定、狀態管理、API 包裝等情境下相當常見;對於 Node.js 開發者,則常用於建立乾淨的類別介面或實作虛擬屬性。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,帶你一步步掌握 JavaScript 的 getter / setter,並提供實務上的應用示例,適合 初學者到中級開發者 閱讀。
核心概念
1. 什麼是 Getter / Setter?
- Getter:當程式碼讀取物件的某個屬性時,自動呼叫的函式。它必須回傳一個值,作為屬性的「讀取結果」。
- Setter:當程式碼對物件的某個屬性賦值時,自動呼叫的函式。它接收一個參數(新值),可以在裡面做驗證、轉換或觸發其他行為。
兩者都是 屬性描述子(property descriptor) 的一部分,可在 物件字面量、Object.defineProperty、或 類別(class) 中定義。
2. 定義方式比較
| 方法 | 語法範例 | 何時使用 |
|---|---|---|
| 物件字面量 (ES5) | let obj = { get foo() { … }, set foo(v) { … } } |
快速建立簡單物件 |
Object.defineProperty |
Object.defineProperty(obj, 'foo', { get(){…}, set(v){…}, configurable:true }) |
需要更細緻的屬性控制(enumerable、configurable) |
| 類別 (ES6) | class C { get foo(){…} set foo(v){…} } |
建立可重用的類別或建構子函式 |
以下分別示範這三種寫法。
3. 程式碼範例
3.1 物件字面量的 Getter / Setter
// 建立一個簡易的座標物件,x、y 只能是數字,超出範圍自動限制
const point = {
_x: 0,
_y: 0,
get x() {
// 讀取時直接回傳內部儲存的值
return this._x;
},
set x(value) {
// 寫入時先檢查類型,再限制在 0~100 之間
if (typeof value !== 'number') {
throw new TypeError('x 必須是數字');
}
this._x = Math.min(Math.max(value, 0), 100);
},
get y() {
return this._y;
},
set y(value) {
if (typeof value !== 'number') {
throw new TypeError('y 必須是數字');
}
this._y = Math.min(Math.max(value, 0), 100);
}
};
point.x = 120; // 超出範圍,實際儲存為 100
point.y = -20; // 小於 0,實際儲存為 0
console.log(point.x, point.y); // 100 0
重點:Getter / Setter 本身不佔用實際的屬性欄位,常會以
_開頭的「私有」變數作為儲存位置。
3.2 Object.defineProperty 的進階用法
const user = {};
// 定義一個只能讀取的 fullName 虛擬屬性
Object.defineProperty(user, 'fullName', {
get() {
return `${this.firstName} ${this.lastName}`;
},
// 沒有 set,等同於唯讀
enumerable: true, // 可列舉(for...in、Object.keys)
configurable: true // 之後可以再修改或刪除
});
user.firstName = 'Ada';
user.lastName = 'Lovelace';
console.log(user.fullName); // Ada Lovelace
技巧:使用
enumerable: true可以讓虛擬屬性在JSON.stringify時被序列化,否則會被忽略。
3.3 類別(Class)中的 Getter / Setter
class Rectangle {
constructor(width, height) {
this.width = width; // 直接呼叫 setter
this.height = height;
}
// 寬度的 getter / setter
get width() {
return this._width;
}
set width(value) {
if (value <= 0) throw new RangeError('寬度必須大於 0');
this._width = value;
}
// 高度的 getter / setter(同上)
get height() {
return this._height;
}
set height(value) {
if (value <= 0) throw new RangeError('高度必須大於 0');
this._height = value;
}
// 計算面積的唯讀屬性
get area() {
return this._width * this._height;
}
}
const r = new Rectangle(5, 3);
console.log(r.area); // 15
r.width = -2; // 拋出 RangeError
說明:
area只實作 getter,代表它是一個 唯讀 的衍生屬性,外部無法直接寫入。
3.4 結合 Proxy 實作動態 Getter / Setter
有時候想要一次為多個屬性加上相同的驗證或轉換,使用 Proxy 可以更靈活。
function createValidatedObject(schema) {
const target = {};
return new Proxy(target, {
get(obj, prop) {
// 若有自訂 getter,直接返回
if (typeof schema[prop]?.get === 'function') {
return schema[prop].get.call(obj);
}
return obj[prop];
},
set(obj, prop, value) {
const rule = schema[prop];
if (rule) {
// 先執行自訂驗證
if (typeof rule.validate === 'function' && !rule.validate(value)) {
throw new TypeError(`屬性 ${prop} 的值不符合驗證規則`);
}
// 若有自訂 setter,呼叫它
if (typeof rule.set === 'function') {
rule.set.call(obj, value);
return true;
}
}
obj[prop] = value;
return true;
}
});
}
// 使用範例
const person = createValidatedObject({
age: {
validate: v => Number.isInteger(v) && v >= 0,
set(v) { this._age = v; },
get() { return this._age; }
},
name: {
set(v) { this._name = v.trim(); },
get() { return this._name; }
}
});
person.age = 30; // 合法
person.name = ' Alice ';
console.log(person.age, person.name); // 30 Alice
// person.age = -5; // 會拋出 TypeError
實務意義:這類模式常見於 ORM、表單驗證 或 設定檔管理,可一次性為整個物件加上統一的驗證/轉換邏輯。
3.5 讓 Getter / Setter 與 JSON.stringify 搭配
class Person {
constructor(first, last) {
this.firstName = first;
this.lastName = last;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// 讓 fullName 在序列化時出現
toJSON() {
return {
firstName: this.firstName,
lastName: this.lastName,
fullName: this.fullName // 手動加入
};
}
}
const p = new Person('John', 'Doe');
console.log(JSON.stringify(p)); // {"firstName":"John","lastName":"Doe","fullName":"John Doe"}
提示:Getter 本身不會被
JSON.stringify包含,若需要序列化,請實作toJSON或在Object.defineProperty時設定enumerable: true並使用 臨時屬性。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 遞迴呼叫 | 在 getter / setter 內直接使用同名屬性會造成無限遞迴。 | 使用 私有變數(如 _prop)或 Symbol 作為實際存儲鍵。 |
| enumerable 為 false | 預設 getter / setter 的屬性不可列舉,導致 for...in、Object.keys 無法看到。 |
明確設定 enumerable: true(Object.defineProperty)或在類別中使用 Object.defineProperty 手動調整。 |
| 不可寫 (writable) 與 setter 同時存在 | 若同時設定 writable: false,setter 仍會被呼叫,卻不會改變值,易造成混淆。 |
只在需要「唯讀」時僅提供 getter,或使用 configurable: false 鎖定屬性。 |
| 序列化遺失 | JSON.stringify 不會自動呼叫 getter。 |
實作 toJSON 方法或在 getter 上加 enumerable: true 並使用臨時屬性。 |
| 效能考量 | 每次存取都會執行函式,過度使用可能影響效能(特別在大量迴圈中)。 | 僅在必要時使用,對於頻繁且簡單的屬性直接使用普通欄位。 |
最佳實踐
- 保持單一職責:Getter 應只負責回傳值,Setter 應只負責驗證與賦值,避免在其中加入大量業務邏輯。
- 使用私有欄位:ES2022 以後可直接使用
#privateField,更安全且語法清晰。 - 文件化每個 getter/setter:即使語法簡潔,也應在註解中說明 為何 需要此驗證或計算。
- 避免在 constructor 內直接寫入同名屬性:若使用 setter 內有副作用,建議在 constructor 中使用私有欄位繞過,或在
super()後再設定。 - 測試覆蓋:針對每個 setter 的驗證邏輯、每個 getter 的計算結果寫單元測試,確保未來改動不會破壞行為。
實際應用場景
| 場景 | 為何適合使用 Getter / Setter |
|---|---|
| 表單資料雙向綁定 | 當使用者輸入時自動驗證、格式化;讀取時自動轉換顯示格式(如日期、金額)。 |
| API 回傳資料的包裝 | 讓外部呼叫者只看到乾淨的屬性名稱,內部可自行處理原始欄位或做緩存。 |
| 計算屬性(Derived Property) | 例如 area、fullName、isAdult 等,避免重複計算且保持即時更新。 |
| 設定檔管理 | 讀取設定時自動補全預設值,寫入時自動寫入磁碟或觸發事件。 |
| 物件監控與偵錯 | 透過 setter 捕捉不當賦值,並在開發環境輸出警告或堆疊資訊。 |
| 封裝第三方庫 | 為不支援 getter/setter 的舊版物件加上一層代理,提供更友好的 API。 |
案例:在 Vue 2.x 中,
data物件會被 Vue 轉成 getter / setter,以便在資料改變時自動觸發 DOM 更新。這正是 getter / setter 在框架層面的典型應用。
總結
- Getter / Setter 是 JavaScript 物件層面的「存取控制」機制,讓屬性讀寫可以伴隨驗證、計算或副作用。
- 可透過 物件字面量、
Object.defineProperty、或類別 三種方式實作,依需求選擇最適合的寫法。 - 使用時需注意 避免遞迴、正確設定列舉性、處理序列化 等常見陷阱,並遵循 單一職責、私有欄位、完整文件與測試 的最佳實踐。
- 在 表單綁定、API 包裝、計算屬性、設定管理、偵錯監控 等實務情境中,getter / setter 能顯著提升程式碼的可讀性、可維護性與安全性。
掌握了 getter / setter,你就能在 JavaScript 中寫出更 乾淨、彈性且易於擴充 的物件程式碼。祝你在開發旅程中玩得開心,寫出高品質的程式!