在 JavaScript 中,Getter (獲取器) 和 Setter (設置器) 是一對特殊的方法,它們允許您像訪問普通屬性一樣來讀取和修改對象的屬性,但在背後,它們執行的是自定義的函數邏輯。這些特殊方法被稱為訪問器屬性 (Accessor Properties),與直接存儲值的數據屬性 (Data Properties) 相對。
Getter 和 Setter 是實現封裝 (Encapsulation) 的關鍵工具,它們提供了對對象屬性的更精細控制,使得我們能夠:
- 保護數據完整性: 在屬性被修改前進行驗證。
- 計算派生值: 動態生成屬性值。
- 執行副作用: 在屬性訪問或修改時觸發其他操作。
- 實現兼容性: 為舊版數據結構提供新接口。
本教程將深入探討 Getter 和 Setter 的工作原理、不同的實現語法(對象字面量、Object.defineProperty()、ES6 class 語法),以及它們在實際開發中的廣泛應用和最佳實踐。通過本教程,您將能夠有效地利用 Getter 和 Setter 來構建更健壯、更具彈性的 JavaScript 對象。
前置知識
在開始本教程之前,建議您具備以下 JavaScript 基礎知識:
- 對象 (Objects): 瞭解對象的創建、屬性訪問 (
obj.prop或obj['prop'])。 - 函數 (Functions): 熟悉函數的聲明、調用、參數以及
return語句。 this關鍵字: 深入理解this在不同上下文中的動態綁定。- 封裝 (Encapsulation): 對封裝的基本概念有初步理解,它如何與數據保護相關。
class語法 (ES6): 瞭解類的基本聲明、constructor方法和實例方法的定義。- 私有類字段 (
#語法,ES2022+): 對其基本概念有初步瞭解,因為它常與 Getter/Setter 結合使用。
目錄
- Getter 和 Setter 簡介
- 1.1 什麼是訪問器屬性 (Accessor Properties)?
- 1.2 為什麼需要 Getter 和 Setter? (核心用例)
- JavaScript 中 Getter 和 Setter 的實現方式
- 2.1 對象字面量 (Object Literals) 和
Object.prototype.__defineGetter__/__defineSetter__(歷史方法)
- 簡潔的語法糖
_propertyName約定
- 2.2
Object.defineProperty()(ES5)
- 更強大的控制力
- 屬性描述符 (
configurable,enumerable,get,set) - 結合私有變量實現
- 2.3
class語法 (ES6)
- 類中的 Getter 和 Setter 定義
- 與
#私有類字段的結合
- Getter 和 Setter 的核心應用場景
- 3.1 封裝與數據驗證 (Validation)
- 3.2 計算屬性 (Computed/Derived Properties)
- 3.3 惰性計算 (Lazy Evaluation)
- 3.4 屬性修改時的副作用 (Side Effects)
- 3.5 提供向後兼容性 (Backward Compatibility)
- Getter 和 Setter 與普通方法的區別
- 4.1 語法上的差異
- 語義上的差異
- 何時使用 Getter/Setter,何時使用普通方法
- 常見陷阱與最佳實踐
- 5.1 避免無限遞歸
- 5.2 私有屬性的命名約定 (
_或#) - 5.3 只讀屬性 (Getter Only)
- 5.4 只寫屬性 (Setter Only) (不常見,但可行)
- 5.5 性能考慮
- 總結
- 附錄:術語表
1. Getter 和 Setter 簡介
1.1 什麼是訪問器屬性 (Accessor Properties)?
JavaScript 中的對象屬性分為兩種:
- 數據屬性 (Data Properties): 直接存儲一個值。例如
obj = { name: "Alice" }中的name。 - 訪問器屬性 (Accessor Properties): 不直接存儲值,而是由一對(或單個)Getter/Setter 函數定義。當訪問或修改這個“屬性”時,實際上是在調用相應的函數。
Getter 是一個函數,當您嘗試讀取一個屬性的值時,它會被調用。
Setter 是一個函數,當您嘗試設置一個屬性的值時,它會被調用。
它們允許您像操作普通數據屬性一樣操作這些屬性,但背後卻隱藏了複雜的邏輯。
1.2 為什麼需要 Getter 和 Setter? (核心用例)
Getter 和 Setter 是實現 JavaScript 中良好面向對象設計和封裝的關鍵:
- 數據驗證和保護: 在設置屬性值之前,可以進行數據驗證,確保數據的有效性,防止無效或不安全的值被賦給屬性。
- 計算屬性: 屬性的值不是預先存儲的,而是在訪問時動態計算得出的。這對於那些依賴其他屬性或外部狀態的值非常有用。
- 隱藏內部實現: 將實際存儲數據的內部變量隱藏起來(通常通過私有命名約定或私有類字段),只通過 Getter/Setter 暴露一個公共接口。
- 副作用: 在屬性被訪問或修改時,可以觸發其他的行為,例如日誌記錄、更新 UI 或觸發其他數據模型變更。
- 兼容性: 為舊版代碼提供一個兼容的屬性接口,而實際內部邏輯可能已完全改變。
2. JavaScript 中 Getter 和 Setter 的實現方式
JavaScript 提供了多種方式來定義 Getter 和 Setter。
2.1 對象字面量 (Object Literals) 和 Object.prototype.__defineGetter__ / __defineSetter__
這是最直觀且現代常用的語法。
const person = {
firstName: 'John',
lastName: 'Doe',
// Getter:當訪問 person.fullName 時,此函數被調用
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter:當設置 person.fullName = 'Jane Smith' 時,此函數被調用
set fullName(value) {
const parts = value.split(' ');
this.firstName = parts[0] || '';
this.lastName = parts[1] || '';
}
};
console.log(person.fullName); // John Doe (調用 Getter)
person.fullName = 'Jane Smith'; // 調用 Setter
console.log(person.firstName); // Jane
console.log(person.lastName); // Smith
console.log(person.fullName); // Jane Smith (再次調用 Getter)
歷史方法 (__defineGetter__ / __defineSetter__):
在 ES5 之前,可以通過這些非標準的屬性來定義 Getter/Setter,但在現代代碼中應避免使用,因為它們已經被廢棄,且不如 Object.defineProperty() 或對象字面量語法靈活和推薦。
// 不推薦在現代代碼中使用
// const obj = {};
// obj.__defineGetter__('myProp', function() { return 'Hello'; });
// obj.__defineSetter__('myProp', function(value) { console.log('Setting:', value); });
// console.log(obj.myProp); // Hello
// obj.myProp = 'World'; // Setting: World
2.2 Object.defineProperty() (ES5)
Object.defineProperty() 方法允許您對對象的屬性進行精確控制,包括定義 Getter 和 Setter,以及其他屬性特性(如可寫性、可枚舉性、可配置性)。這是在 ES5 中創建 Getter/Setter 的標準方式。
function createTemperature() {
let _celsius = 0; // 私有變量,使用下劃線約定
const temp = {};
Object.defineProperty(temp, 'celsius', {
enumerable: true, // 允許被 for...in 或 Object.keys() 枚舉
configurable: true, // 允許被 delete 或再次 defineProperty 修改
get: function() {
console.log('Fetching Celsius...');
return _celsius;
},
set: function(newVal) {
if (typeof newVal === 'number' && newVal >= -273.15) { // 數據驗證
console.log('Setting Celsius...');
_celsius = newVal;
} else {
console.warn('Invalid Celsius value!');
}
}
});
Object.defineProperty(temp, 'fahrenheit', {
enumerable: true,
configurable: true,
get: function() {
console.log('Calculating Fahrenheit...');
return (_celsius * 9/5) + 32;
},
set: function(newFahrenheit) {
console.log('Setting Fahrenheit...');
temp.celsius = (newFahrenheit - 32) * 5/9; // 通過 Setter 調用另一個 Setter
}
});
return temp;
}
const t = createTemperature();
console.log(t.celsius); // Fetching Celsius... 0
t.celsius = 25; // Setting Celsius...
console.log(t.fahrenheit); // Calculating Fahrenheit... 77
t.fahrenheit = 68; // Setting Fahrenheit... Setting Celsius...
console.log(t.celsius); // Fetching Celsius... 20
t.celsius = -300; // Invalid Celsius value!
console.log(t.celsius); // Fetching Celsius... 20
Object.defineProperty() 更加靈活,特別適用於在運行時動態地添加或修改屬性,或者需要精細控制屬性特性時。
2.3 class 語法 (ES6)
在 ES6 class 語法中,定義 Getter 和 Setter 與在對象字面量中非常相似,直接在類體內定義即可。
class Circle {
#radius; // 使用私有類字段(ES2022+)來存儲實際的半徑值
constructor(initialRadius) {
// 構造函數中設置值,會觸發 setter
this.radius = initialRadius;
}
// Getter:當訪問 circle.radius 時,此函數被調用
get radius() {
console.log('Getting radius...');
return this.#radius;
}
// Setter:當設置 circle.radius = value 時,此函數被調用
set radius(value) {
if (typeof value === 'number' && value >= 0) {
console.log('Setting radius...');
this.#radius = value;
} else {
console.warn('Radius must be a non-negative number.');
}
}
// 計算屬性:周長(依賴半徑)
get circumference() {
console.log('Calculating circumference...');
return 2 * Math.PI * this.radius; // 調用 Getter,而不是直接訪問 #radius
}
// 計算屬性:面積(依賴半徑)
get area() {
console.log('Calculating area...');
return Math.PI * this.radius * this.radius; // 調用 Getter
}
}
const myCircle = new Circle(10); // Setting radius...
console.log(myCircle.radius); // Getting radius... 10
console.log(myCircle.circumference); // Calculating circumference... Getting radius... 62.83...
console.log(myCircle.area); // Calculating area... Getting radius... 314.15...
myCircle.radius = 12; // Setting radius...
myCircle.radius = -5; // Radius must be a non-negative number. (值不會被修改)
console.log(myCircle.radius); // Getting radius... 12
在類中使用 Getter/Setter 時,通常會結合私有類字段 (#radius) 來存儲實際的數據,從而實現更徹底的封裝。
3. Getter 和 Setter 的核心應用場景
3.1 封裝與數據驗證 (Validation)
這是 Getter 和 Setter 最常見的用途之一。它們允許您在設置屬性時執行驗證邏輯,確保屬性值的有效性。
class User {
#age;
constructor(name, age) {
this.name = name;
this.age = age; // 調用 Setter
}
get age() {
return this.#age;
}
set age(value) {
if (typeof value === 'number' && value >= 0 && value <= 150) {
this.#age = value;
} else {
console.error('Invalid age value. Age must be a number between 0 and 150.');
// 可以拋出錯誤,或者設置默認值
this.#age = 0; // 設為默認值
}
}
}
const user1 = new User('Alice', 30);
console.log(user1.age); // 30
user1.age = -10; // Invalid age value. Age must be a number between 0 and 150.
console.log(user1.age); // 0 (被 Setter 重置為默認值)
user1.age = 200; // Invalid age value. Age must be a number between 0 and 150.
console.log(user1.age); // 0
user1.age = 25;
console.log(user1.age); // 25
3.2 計算屬性 (Computed/Derived Properties)
當一個屬性的值依賴於其他一個或多個屬性時,可以使用 Getter 來動態計算這個值,而無需實際存儲它。
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() {
return this.width * this.height; // 每次訪問時動態計算
}
get perimeter() {
return 2 * (this.width + this.height); // 每次訪問時動態計算
}
}
const rect = new Rectangle(10, 5);
console.log(`Area: ${rect.area}`); // Area: 50
console.log(`Perimeter: ${rect.perimeter}`); // Perimeter: 30
rect.width = 15;
console.log(`New Area: ${rect.area}`); // New Area: 75 (area 會自動重新計算)
3.3 惰性計算 (Lazy Evaluation)
Getter 可以用來實現惰性計算,即只有在首次訪問屬性時才執行昂貴的計算,並將結果緩存起來。
class ExpensiveCalculation {
#data;
#_cachedResult = null; // 用於緩存結果的私有變量
constructor(data) {
this.#data = data;
}
// 惰性計算的 Getter
get processedData() {
if (this.#_cachedResult === null) {
console.log('Performing expensive calculation...');
// 模擬耗時計算
this.#_cachedResult = this.#data.map(item => item * 2).reduce((sum, val) => sum + val, 0);
}
return this.#_cachedResult;
}
// 如果原始數據改變,需要重置緩存
resetData(newData) {
this.#data = newData;
this.#_cachedResult = null; // 重置緩存
}
}
const calc = new ExpensiveCalculation([1, 2, 3]);
console.log(calc.processedData); // Performing expensive calculation... 12
console.log(calc.processedData); // 12 (第二次訪問直接返回緩存,不重新計算)
calc.resetData([4, 5, 6]);
console.log(calc.processedData); // Performing expensive calculation... 30 (數據改變,重新計算)
3.4 屬性修改時的副作用 (Side Effects)
Setter 可以在屬性被修改時觸發其他邏輯,例如更新 UI、發送事件或執行日誌記錄。
class LightSwitch {
#isOn = false;
#callbacks = []; // 存儲訂閲者的數組
addChangeListener(callback) {
this.#callbacks.push(callback);
}
get isOn() {
return this.#isOn;
}
set isOn(value) {
if (typeof value === 'boolean' && value !== this.#isOn) {
this.#isOn = value;
console.log(`Light is now ${this.#isOn ? 'ON' : 'OFF'}.`);
// 觸發副作用:通知所有監聽者
this.#callbacks.forEach(cb => cb(this.#isOn));
}
}
}
const livingRoomLight = new LightSwitch();
// 訂閲狀態變化
livingRoomLight.addChangeListener(status => {
console.log(`UI Component: Light status changed to ${status ? 'bright' : 'dark'}.`);
});
livingRoomLight.isOn = true; // Light is now ON. \n UI Component: Light status changed to bright.
livingRoomLight.isOn = false; // Light is now OFF. \n UI Component: Light status changed to dark.
livingRoomLight.isOn = false; // 不會觸發副作用,因為值未改變
3.5 提供向後兼容性 (Backward Compatibility)
當您重構舊代碼或更改數據結構時,可以使用 Getter 和 Setter 來提供一個兼容舊接口的視圖。
// 假設舊的 User 對象直接暴露 _firstName 和 _lastName
// class OldUser {
// constructor(fName, lName) {
// this._firstName = fName;
// this._lastName = lName;
// }
// }
// 新的 User 對象希望提供 fullName 接口,並使用新的內部存儲
class NewUser {
#fullName; // 內部使用完整姓名存儲
constructor(firstName, lastName) {
this.#fullName = `${firstName} ${lastName}`;
}
// 為兼容舊代碼,提供 firstName 和 lastName 的 Getter/Setter
get firstName() {
return this.#fullName.split(' ')[0];
}
set firstName(value) {
const parts = this.#fullName.split(' ');
this.#fullName = `${value} ${parts[1] || ''}`;
}
get lastName() {
return this.#fullName.split(' ')[1] || '';
}
set lastName(value) {
const parts = this.#fullName.split(' ');
this.#fullName = `${parts[0]} ${value}`;
}
// 現代接口
get fullName() {
return this.#fullName;
}
set fullName(value) {
this.#fullName = value;
}
}
const user = new NewUser('Alice', 'Wonderland');
console.log(user.firstName); // Alice
console.log(user.lastName); // Wonderland
console.log(user.fullName); // Alice Wonderland
user.firstName = 'Betty';
console.log(user.fullName); // Betty Wonderland
4. Getter 和 Setter 與普通方法的區別
4.1 語法上的差異
- Getter/Setter: 使用
get或set關鍵字,像屬性一樣訪問(obj.prop)。 - 普通方法: 使用函數名,像函數一樣調用(
obj.method())。
4.2 語義上的差異
- Getter/Setter: 主要用於獲取或設置與對象狀態緊密相關的值,通常這些值在外部看起來像數據屬性。它們是屬性訪問的“語法糖”。
- 普通方法: 主要用於執行操作、行為,或者返回獨立於對象狀態的更復雜結果。
4.3 何時使用 Getter/Setter,何時使用普通方法
- 使用 Getter/Setter 當:
- 您需要像訪問數據屬性一樣讀取或寫入一個值,但背後需要執行驗證、計算或副作用。
- 這個“屬性”在語義上感覺更像一個數據項而不是一個動作。
- 例如:
user.age,product.price,rectangle.area。
- 使用普通方法當:
- 您需要執行一個有明顯“動作”含義的操作。
- 操作可能接受多個參數,並且不僅僅是設置一個值。
- 操作的結果可能不直接映射到對象的某個單一屬性。
- 例如:
user.authenticate(password),database.save(),httpClient.fetchData(url).
經驗法則: 如果讀取或寫入屬性不會改變對象的其他可見狀態,並且只是返回一個值或設置一個值,那麼 Getter/Setter 可能是合適的。如果操作涉及到更復雜的行為或副作用,普通方法通常更清晰。
5. 常見陷阱與最佳實踐
5.1 避免無限遞歸
陷阱: 在 Getter 或 Setter 中直接引用同名的 Getter 或 Setter 屬性,會導致無限遞歸調用。
class BadExample {
constructor(value) {
this.value = value; // 這裏會調用 setter
}
get value() {
return this.value; // 無限遞歸調用 getter 自己
}
set value(newValue) {
this.value = newValue; // 無限遞歸調用 setter 自己
}
}
// const bad = new BadExample(10); // Uncaught RangeError: Maximum call stack size exceeded
最佳實踐: 始終使用一個獨立的私有變量或私有類字段來存儲實際的數據。
class GoodExample {
#value; // 使用私有類字段
constructor(value) {
this.value = value; // 調用 setter
}
get value() {
return this.#value; // 讀取私有字段
}
set value(newValue) {
this.#value = newValue; // 寫入私有字段
}
}
const good = new GoodExample(10); // 正常工作
console.log(good.value); // 10
5.2 私有屬性的命名約定 (_ 或 #)
_propertyName(下劃線前綴): 傳統的約定,表示這是一個內部使用的屬性,不應從外部直接訪問。但這只是約定,JavaScript 運行時不強制執行。#propertyName(井號前綴): ES2022+ 的私有類字段語法,提供了語言層面的強制私有性,是目前在類中實現真正私有屬性的最佳方式。
最佳實踐: 在類中使用 # 私有類字段。在對象字面量或其他需要私有性的場景中,如果不需要嚴格的運行時強制,可以繼續使用 _ 約定,或者利用閉包。
5.3 只讀屬性 (Getter Only)
如果一個屬性只需要在訪問時計算或提供值,而外部不應該能夠直接修改它,則只定義 Getter 即可。
class Product {
constructor(name, price) {
this._name = name;
this._price = price;
}
get name() { return this._name; }
get price() { return this._price; }
get tax() { return this._price * 0.05; } // 只讀計算屬性
get finalPrice() { return this._price + this.tax; } // 只讀計算屬性
}
const item = new Product('Book', 20);
console.log(item.finalPrice); // 21
// item.tax = 1; // TypeError: Cannot set property tax of #<Product> which has only a getter
5.4 只寫屬性 (Setter Only) (不常見,但可行)
如果一個屬性只需要外部寫入,而讀取則沒有意義或被禁止,則只定義 Setter。這種情況相對少見,但例如,一個用於接收一次性密碼或觸發某個事件的屬性可能適用。
class EventLogger {
set logMessage(message) {
console.log(`[LOG ${new Date().toISOString()}] ${message}`);
}
}
const logger = new EventLogger();
logger.logMessage = "Application started."; // 觸發 Setter
// console.log(logger.logMessage); // undefined (沒有 getter)
5.5 性能考慮
Getter 和 Setter 是函數調用,會比直接訪問數據屬性略慢。然而,在大多數現代 JavaScript 引擎中,這種性能差異通常可以忽略不計,除非是在極度性能敏感的循環中頻繁訪問。
最佳實踐: 優先考慮代碼的清晰性、可維護性和正確性。只有在確實遇到性能瓶頸時,才考慮優化 Getter/Setter。
6. 總結
Getter 和 Setter (訪問器屬性) 是 JavaScript 面向對象編程中實現封裝和數據保護的強大工具。它們允許您:
- 通過像訪問普通屬性一樣的語法,在背後執行自定義的函數邏輯。
- 在設置屬性值時進行數據驗證,確保數據完整性。
- 動態計算屬性值,實現計算屬性和惰性計算。
- 在屬性訪問或修改時觸發副作用。
- 為對象提供更清晰、更受控的公共接口。
本教程詳細介紹了 Getter 和 Setter 在對象字面量、Object.defineProperty() 和 ES6 class 語法中的不同實現方式,探討了它們的多種應用場景,並提供了避免常見陷阱的最佳實踐。熟練掌握 Getter 和 Setter 將使您能夠編寫出更健壯、更具表現力且更易於維護的 JavaScript 代碼。
7. 附錄:術語表
- Getter (獲取器): 一個特殊函數,當讀取對象的某個屬性時被調用。使用
get關鍵字定義。 - Setter (設置器): 一個特殊函數,當設置對象的某個屬性時被調用。使用
set關鍵字定義。 - 訪問器屬性 (Accessor Properties): 由 Getter 和/或 Setter 函數定義的屬性,而不是直接存儲值的屬性。
- 數據屬性 (Data Properties): 直接存儲一個值的屬性。
- 封裝 (Encapsulation): OOP 原則,將數據和操作數據的方法捆綁成一個單元,並隱藏內部狀態。Getter/Setter 是實現封裝的重要手段。
Object.defineProperty(): JavaScript 方法,用於直接在一個對象上定義新屬性,或者修改現有屬性,並返回該對象。允許精確控制屬性的特性。- 屬性描述符 (Property Descriptor): 用來描述對象屬性特性的對象,包含
value,writable,get,set,enumerable,configurable等字段。 class語法 (ES6): JavaScript 中用於定義類(對象藍圖)的語法糖。- 私有類字段 (Private Class Fields): ES2022+ 引入的語法,使用
#前綴定義,只能在定義它們的類內部訪問,實現真正的私有性。 - 惰性計算 (Lazy Evaluation): 延遲計算一個值,直到該值首次被需要時才執行計算。
- 副作用 (Side Effect): 在執行某個操作(如屬性賦值)時,除了改變操作本身的結果,還對外部環境產生其他可觀察的影響。
- 無限遞歸 (Infinite Recursion): 函數無限次地調用自身,最終導致調用棧溢出錯誤。
_propertyName: 傳統上用於表示私有或內部使用的屬性的命名約定。