在 JavaScript 中,Getter (獲取器)Setter (設置器) 是一對特殊的方法,它們允許您像訪問普通屬性一樣來讀取和修改對象的屬性,但在背後,它們執行的是自定義的函數邏輯。這些特殊方法被稱為訪問器屬性 (Accessor Properties),與直接存儲值的數據屬性 (Data Properties) 相對。

Getter 和 Setter 是實現封裝 (Encapsulation) 的關鍵工具,它們提供了對對象屬性的更精細控制,使得我們能夠:

  • 保護數據完整性: 在屬性被修改前進行驗證。
  • 計算派生值: 動態生成屬性值。
  • 執行副作用: 在屬性訪問或修改時觸發其他操作。
  • 實現兼容性: 為舊版數據結構提供新接口。

本教程將深入探討 Getter 和 Setter 的工作原理、不同的實現語法(對象字面量、Object.defineProperty()、ES6 class 語法),以及它們在實際開發中的廣泛應用和最佳實踐。通過本教程,您將能夠有效地利用 Getter 和 Setter 來構建更健壯、更具彈性的 JavaScript 對象。


前置知識

在開始本教程之前,建議您具備以下 JavaScript 基礎知識:

  • 對象 (Objects): 瞭解對象的創建、屬性訪問 (obj.propobj['prop'])。
  • 函數 (Functions): 熟悉函數的聲明、調用、參數以及 return 語句。
  • this 關鍵字: 深入理解 this 在不同上下文中的動態綁定。
  • 封裝 (Encapsulation): 對封裝的基本概念有初步理解,它如何與數據保護相關。
  • class 語法 (ES6): 瞭解類的基本聲明、constructor 方法和實例方法的定義。
  • 私有類字段 (# 語法,ES2022+): 對其基本概念有初步瞭解,因為它常與 Getter/Setter 結合使用。

目錄

  1. Getter 和 Setter 簡介
  • 1.1 什麼是訪問器屬性 (Accessor Properties)?
  • 1.2 為什麼需要 Getter 和 Setter? (核心用例)
  1. 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 定義
  • # 私有類字段的結合
  1. Getter 和 Setter 的核心應用場景
  • 3.1 封裝與數據驗證 (Validation)
  • 3.2 計算屬性 (Computed/Derived Properties)
  • 3.3 惰性計算 (Lazy Evaluation)
  • 3.4 屬性修改時的副作用 (Side Effects)
  • 3.5 提供向後兼容性 (Backward Compatibility)
  1. Getter 和 Setter 與普通方法的區別
  • 4.1 語法上的差異
  • 語義上的差異
  • 何時使用 Getter/Setter,何時使用普通方法
  1. 常見陷阱與最佳實踐
  • 5.1 避免無限遞歸
  • 5.2 私有屬性的命名約定 (_#)
  • 5.3 只讀屬性 (Getter Only)
  • 5.4 只寫屬性 (Setter Only) (不常見,但可行)
  • 5.5 性能考慮
  1. 總結
  2. 附錄:術語表

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 中良好面向對象設計和封裝的關鍵:

  1. 數據驗證和保護: 在設置屬性值之前,可以進行數據驗證,確保數據的有效性,防止無效或不安全的值被賦給屬性。
  2. 計算屬性: 屬性的值不是預先存儲的,而是在訪問時動態計算得出的。這對於那些依賴其他屬性或外部狀態的值非常有用。
  3. 隱藏內部實現: 將實際存儲數據的內部變量隱藏起來(通常通過私有命名約定或私有類字段),只通過 Getter/Setter 暴露一個公共接口。
  4. 副作用: 在屬性被訪問或修改時,可以觸發其他的行為,例如日誌記錄、更新 UI 或觸發其他數據模型變更。
  5. 兼容性: 為舊版代碼提供一個兼容的屬性接口,而實際內部邏輯可能已完全改變。

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: 使用 getset 關鍵字,像屬性一樣訪問(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 傳統上用於表示私有或內部使用的屬性的命名約定。