本文 AI 產出,尚未審核

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...inObject.keys 無法看到。 明確設定 enumerable: trueObject.defineProperty)或在類別中使用 Object.defineProperty 手動調整。
不可寫 (writable) 與 setter 同時存在 若同時設定 writable: false,setter 仍會被呼叫,卻不會改變值,易造成混淆。 只在需要「唯讀」時僅提供 getter,或使用 configurable: false 鎖定屬性。
序列化遺失 JSON.stringify 不會自動呼叫 getter。 實作 toJSON 方法或在 getter 上加 enumerable: true 並使用臨時屬性。
效能考量 每次存取都會執行函式,過度使用可能影響效能(特別在大量迴圈中)。 僅在必要時使用,對於頻繁且簡單的屬性直接使用普通欄位。

最佳實踐

  1. 保持單一職責:Getter 應只負責回傳值,Setter 應只負責驗證與賦值,避免在其中加入大量業務邏輯。
  2. 使用私有欄位:ES2022 以後可直接使用 #privateField,更安全且語法清晰。
  3. 文件化每個 getter/setter:即使語法簡潔,也應在註解中說明 為何 需要此驗證或計算。
  4. 避免在 constructor 內直接寫入同名屬性:若使用 setter 內有副作用,建議在 constructor 中使用私有欄位繞過,或在 super() 後再設定。
  5. 測試覆蓋:針對每個 setter 的驗證邏輯、每個 getter 的計算結果寫單元測試,確保未來改動不會破壞行為。

實際應用場景

場景 為何適合使用 Getter / Setter
表單資料雙向綁定 當使用者輸入時自動驗證、格式化;讀取時自動轉換顯示格式(如日期、金額)。
API 回傳資料的包裝 讓外部呼叫者只看到乾淨的屬性名稱,內部可自行處理原始欄位或做緩存。
計算屬性(Derived Property) 例如 areafullNameisAdult 等,避免重複計算且保持即時更新。
設定檔管理 讀取設定時自動補全預設值,寫入時自動寫入磁碟或觸發事件。
物件監控與偵錯 透過 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 中寫出更 乾淨、彈性且易於擴充 的物件程式碼。祝你在開發旅程中玩得開心,寫出高品質的程式!