在物件導向程式設計 (OOP) 中,直接開放外部存取類別內部的私有屬性,往往會導致資料狀態不可控、缺乏校驗機制,進而破壞系統的穩定性。Getter (讀取器) 與 Setter (寫入器) 的出現,正是為了解決這個核心痛點。
這兩者本質上是定義在類別中的特殊存取方法,讓開發者能在物件屬性被「讀取」或「寫入」的當下攔截操作,進行資料校驗、動態運算或觸發副作用 (Side Effects),進而達到資料封裝 (Encapsulation) 的目的。
雖然許多前端工程師是在 JavaScript (ES6+) 中首次接觸到這個語法,但存取器 (Accessors) 其實是軟體工程中通用的設計概念,在現代主流語言中皆有對應的實作思維。
為什麼需要 Getter 與 Setter?
如果只看表面的存取行為,直接操作屬性與透過存取器操作似乎相同:
// 直接存取 (不推薦:缺乏控制力)
user.age = -5; // 髒數據成功寫入
// 透過存取器存取 (推薦:具備控制力)
user.age = -5; // 內部攔截,拋出錯誤或拒絕寫入核心優勢
- 資料驗證 (Validation):在資料寫入前,確保其符合業務邏輯 (例如年齡不能為負數)。
- 動態計算屬性 (Computed Properties):屬性值不需要實際存在於記憶體中,而是在讀取時動態計算產生 (例如透過「生日」動態計算出「年齡」)。
- 唯讀或唯寫屬性:只實作 Getter 即可建立合法的唯讀屬性,防止外部惡意修改。
- 追蹤與紀錄 (Logging & Side Effects):在屬性變更時,自動觸發日誌紀錄、狀態同步或 UI 更新。
語法與實作範例
以下針對同一個業務情境 (擁有私有年齡屬性、具備負數校驗邏輯),展示不同語言的標準實作方式,幫助你融會貫通其核心邏輯。
1. JavaScript / TypeScript 實作
使用 get 與 set 關鍵字來定義屬性存取器。配合 ECMAScript 的原生私有欄位 ()#),可以達到完美的封裝,讓存取方法在外部看起來就像操作一般屬性。
// TypeScript
class User {
// 使用 # 宣告原生私有屬性,防止外部直接存取
#age: number = 0;
constructor(age: number) {
this.age = age; // 呼叫 setter 進行初始校驗
}
// Getter: 當執行 user.age 時觸發
get age(): number {
return this.#age;
}
// Setter: 當執行 user.age = value 時觸發
set age(value: number) {
if (value < 0) {
throw new Error('Age cannot be negative.');
}
this.#age = value;
}
}
// 實務應用
const user = new User(25);
console.log(user.age); // 25 (觸發 get)
user.age = 30; // 觸發 set,修改成功
// user.age = -5; // 拋出 Error: Age cannot be negative.2. Python 實作
Python 採用 @property 裝飾器 (Decorator) 來實作 Getter,並使用 @<property_name>.setter 來實作 Setter。這讓程式碼在保持簡潔屬性存取語法的同時,兼具方法封裝的能力。
# Python
class User:
def __init__(self, age: int):
self.age = age # 呼叫 setter 進行初始校驗
@property
def age(self) -> int:
"""Getter 裝飾器"""
return self._age # 慣例上使用單底線表示受保護的私有變數
@age.setter
def age(self, value: int) -> None:
"""Setter 裝飾器"""
if value < 0:
raise ValueError("Age cannot be negative.")
self._age = value
# 實務應用
user = User(25)
print(user.age) # 25 (隱式呼叫 getter)
user.age = 30 # 隱式呼叫 setter3. Java 實作
Java 作為嚴謹的傳統物件導向語言,不具備將方法偽裝成屬性存取的語法糖。Java 遵循標準的 JavaBean 規範,透過明確的 getX() 與 setX() 公開方法來存取 private 欄位。
// Java
public class User {
private int age; // 宣告私有屬性
public User(int age) {
setAge(age); // 呼叫 setter 進行初始校驗
}
// 標準 Getter 方法
public int getAge() {
return this.age;
}
// 標準 Setter 方法
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative.");
}
this.age = age;
}
}
// 實務應用
public class Main {
public static void main(String[] args) {
User user = new User(25);
System.out.println(user.getAge()); // 25 (明確呼叫方法)
user.setAge(30); // 修改成功
}
}核心實務應用場景
場景一:動態計算屬性 (Computed Property)
物件內部不儲存最終值,而是由既有資料即時推導,避免資料同步出錯。
// TypeScript 範例
class Product {
constructor(public price: number, public discount: number) {}
// 唯讀計算屬性:外部只需讀取,不需手動維護實際總價
get finalPrice(): number {
return this.price * (1 - this.discount);
}
}
const item = new Product(100, 0.2);
console.log(item.finalPrice); // 80場景二:觸發副作用 (Side Effects / Reactive Patterns)
在屬性改變的當下,通知系統其他模組進行響應,常見於前端框架的響應式核心或後端狀態機。
// TypeScript 範例
class Task {
#status: string = 'pending';
get status(): string {
return this.#status;
}
set status(newStatus: string) {
const oldStatus = this.#status;
this.#status = newStatus;
// 攔截變更,觸發系統事件
this.onStatusChange(oldStatus, newStatus);
}
private onStatusChange(oldState: string, newState: string) {
console.log(`[LOG] Task state transitioned from ${oldState} to ${newState}`);
}
}技術誤區與陷阱
-
避免無限遞迴 (Stack Overflow): 在 JavaScript 與 Python 的 Setter 中,最常見的錯誤是方法名與內部賦值的屬性名完全相同。
TypeScript// 錯誤示範 set age(value) { this.age = value; // 錯誤:這會再次觸發 set age(),導致無限迴圈與記憶體溢位 }解決方案:內部儲存變數必須使用不同的名稱 (例如原生私有屬性
#age或慣例受保護屬性_age)。 -
效能考量: Getter 內部不應該執行高耗時的複雜運算 (例如大量的 I/O 操作或深層資料遍歷)。因為外部呼叫者從語法上看它只是個「屬性存取」,會預期它在 O(1) 常數時間內返回。耗時操作應明確定義為一般「方法 (Method)」。
總結
Getter 與 Setter 是實踐軟體工程「開放-封閉原則 (Open-Closed Principle)」的重要基礎。它允許類別在不改變外部調用介面的前提下,自由修改內部的資料校驗邏輯與儲存結構。正確使用存取器,能讓你的程式碼在面對業務變更時具備更高的健壯性與可維護性。