本章重點#
- 使用 getter 和 setter 控制物件屬性的存取
- 透過 proxy 控制物件存取
- 使用 proxy 處理橫切關注點 (cross-cutting concerns)
8.1 使用 Getter 和 Setter 控制屬性存取#
JavaScript 物件是動態的屬性集合。在許多場景下(驗證屬性值、記錄日誌、在 UI 顯示資料),我們需要能監控和控制物件的變動。
為什麼需要 Getter/Setter?#
以一個 Ninja 建構函式為例,直接修改 skillLevel 屬性存在以下風險:
- 可能被賦予錯誤型別的值(如
ninja.skillLevel = "high") - 無法記錄屬性的變更歷史
- 無法在屬性變動時自動更新 UI
透過 getter 和 setter,可以將屬性存取包裝為方法呼叫,在讀取和寫入時執行額外邏輯。
8.1.1 定義 Getter 和 Setter#
Getter 和 setter 可以透過兩種方式定義:
- 在 object literal 或 ES6 class 中使用
get和set關鍵字 - 使用
Object.defineProperty方法
Object literal 語法:
const ninjaCollection = {
ninjas: ["Yoshi", "Kuma", "Hattori"],
get firstNinja() {
report("Getting firstNinja");
return this.ninjas[0];
},
set firstNinja(value) {
report("Setting firstNinja");
this.ninjas[0] = value;
},
};
// 像普通屬性一樣存取,但會觸發 getter/setter
ninjaCollection.firstNinja; // 觸發 get
ninjaCollection.firstNinja = "Hachi"; // 觸發 set
ES6 class 語法:
class NinjaCollection {
constructor() {
this.ninjas = ["Yoshi", "Kuma", "Hattori"];
}
get firstNinja() {
return this.ninjas[0];
}
set firstNinja(value) {
this.ninjas[0] = value;
}
}Getter 不接受任何參數;Setter 接受一個參數(賦值表達式的右側值)。存取 getter/setter 屬性時,JavaScript 引擎會自動建立執行上下文並執行對應方法,過程與一般函式呼叫相同。

Figure 8.3: 透過 getter 方法存取屬性時,會隱式呼叫對應的 getter
使用 Object.defineProperty:
當需要控制私有物件屬性時,Object.defineProperty 特別有用,因為 getter/setter 方法可以與私有變數建立在同一個函式作用域中,形成閉包:
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, "skillLevel", {
get: () => {
report("The get method is called");
return _skillLevel;
},
set: (value) => {
report("The set method is called");
_skillLevel = value;
},
});
}
const ninja = new Ninja();
assert(typeof ninja._skillLevel === "undefined"); // 無法直接存取
assert(ninja.skillLevel === 0); // 透過 getter 存取
ninja.skillLevel = 10; // 透過 setter 設值
不需要同時定義 getter 和 setter。若只定義 getter 而嘗試寫入:在非嚴格模式下會被靜默忽略;在嚴格模式下會拋出 TypeError。
8.1.2 使用 Setter 驗證屬性值#
Setter 可以在賦值時驗證傳入值,防止無效資料:
function Ninja() {
let _skillLevel = 0;
Object.defineProperty(this, "skillLevel", {
get: () => _skillLevel,
set: (value) => {
if (!Number.isInteger(value)) {
throw new TypeError("Skill level should be a number");
}
_skillLevel = value;
},
});
}
const ninja = new Ninja();
ninja.skillLevel = 10; // 正常
ninja.skillLevel = "Great"; // 拋出 TypeError
雖然驗證會增加額外開銷,但這是安全使用動態語言所必須付出的代價。相同原則也可應用於追蹤值變更歷史、執行日誌記錄、提供變更通知等。
8.1.3 使用 Getter 和 Setter 定義計算屬性#
Getter 和 setter 可以定義計算屬性 (computed properties),其值不直接儲存,而是依據其他屬性動態計算:
const shogun = {
name: "Yoshiaki",
clan: "Ashikaga",
get fullTitle() {
return this.name + " " + this.clan;
},
set fullTitle(value) {
const segments = value.split(" ");
this.name = segments[0];
this.clan = segments[1];
},
};
assert(shogun.fullTitle === "Yoshiaki Ashikaga");
shogun.fullTitle = "Ieyasu Tokugawa";
assert(shogun.name === "Ieyasu");
assert(shogun.clan === "Tokugawa");當某個值完全取決於物件的內部狀態時,使用計算屬性比
getFullTitle()方法更能提升程式碼的概念清晰度。
8.2 使用 Proxy 控制存取#
Proxy 是一個代理物件 (surrogate),透過它可以控制對另一個物件的存取。你可以將 proxy 視為 getter/setter 的泛化版本:getter/setter 只能控制單一屬性的存取,而 proxy 可以通用地攔截所有對物件的互動,包括方法呼叫。
const emperor = { name: "Komei" };
const representative = new Proxy(emperor, {
get: (target, key) => {
report("Reading " + key + " through a proxy");
return key in target ? target[key] : "Don't bother the emperor!";
},
set: (target, key, value) => {
report("Writing " + key + " through a proxy");
target[key] = value;
},
});
Figure 8.5: 執行 proxy 範例程式碼的輸出結果
Proxy 建構函式接受兩個參數:
- target — 被代理的原始物件
- handler — 定義 traps(陷阱函式)的物件,當特定操作在 proxy 上執行時會觸發
常見的內建 traps:
get/set— 讀取/寫入屬性時觸發apply— 呼叫函式時觸發;construct— 使用new運算子時觸發enumerate—for-in語句時觸發getPrototypeOf/setPrototypeOf— 取得/設定 prototype 時觸發
有些操作無法被 trap 攔截:相等比較(
==/===)、instanceof和typeof。這是因為這些操作的行為不應由使用者自訂函式決定。
8.2.1 使用 Proxy 實現日誌記錄 (Logging)#
傳統做法是在每個屬性的 getter/setter 中加入日誌程式碼,這會讓領域邏輯與日誌程式碼混在一起。使用 proxy 可以優雅地分離:
function makeLoggable(target) {
return new Proxy(target, {
get: (target, property) => {
report("Reading " + property);
return target[property];
},
set: (target, property, value) => {
report("Writing value " + value + " to " + property);
target[property] = value;
},
});
}
let ninja = { name: "Yoshi" };
ninja = makeLoggable(ninja);
ninja.name; // 自動記錄:Reading name
ninja.weapon = "sword"; // 自動記錄:Writing value sword to weapon
日誌邏輯集中在一處定義,可重複用於任意數量的物件,不需要為每個屬性分別加入日誌程式碼。
8.2.2 使用 Proxy 測量效能 (Performance)#
透過 apply trap,可以在不修改原始函式程式碼的情況下測量函式執行時間:
function isPrime(number) {
if (number < 2) {
return false;
}
for (let i = 2; i < number; i++) {
if (number % i === 0) {
return false;
}
}
return true;
}
isPrime = new Proxy(isPrime, {
apply: (target, thisArg, args) => {
console.time("isPrime");
const result = target.apply(thisArg, args);
console.timeEnd("isPrime");
return result;
},
});
isPrime(1299827); // 呼叫方式不變,但自動測量效能
8.2.3 使用 Proxy 自動填充屬性 (Autopopulating Properties)#
Proxy 可以在存取不存在的屬性時自動建立它,避免 null 例外:
function Folder() {
return new Proxy(
{},
{
get: (target, property) => {
report("Reading " + property);
if (!(property in target)) {
target[property] = new Folder(); // 自動建立
}
return target[property];
},
}
);
}
const rootFolder = new Folder();
// 不會拋出例外,中間層級會自動建立
rootFolder.ninjasDir.firstNinjaDir.ninjaFile = "yoshi.txt";8.2.4 使用 Proxy 實現負數陣列索引#
許多語言(Python、Ruby、Perl)支援負數索引從陣列尾部存取元素。JavaScript 原生不支援,但可以透過 proxy 模擬:
function createNegativeArrayProxy(array) {
if (!Array.isArray(array)) {
throw new TypeError("Expected an array");
}
return new Proxy(array, {
get: (target, index) => {
index = +index;
return target[index < 0 ? target.length + index : index];
},
set: (target, index, val) => {
index = +index;
return (target[index < 0 ? target.length + index : index] = val);
},
});
}
const ninjas = ["Yoshi", "Kuma", "Hattori"];
const proxiedNinjas = createNegativeArrayProxy(ninjas);
assert(proxiedNinjas[-1] === "Hattori"); // 從尾部存取
assert(proxiedNinjas[-3] === "Yoshi");8.2.5 Proxy 的效能代價#
Proxy 會為所有操作增加一層間接層,這會顯著影響效能:
- 在 Chrome 中,proxy 大約慢 50 倍
- 在 Firefox 中,大約慢 20 倍
建議謹慎使用 proxy。可以在非效能敏感的程式碼中使用,但避免在頻繁執行的程式碼中使用。務必進行效能測試。
8.3 本章總結#
- 透過 getter、setter 和 proxy 可以監控和控制物件的存取
- Accessor 屬性可透過
Object.defineProperty或 object literal / ES6 class 中的get/set語法定義 get方法在讀取屬性時被隱式呼叫,set方法在寫入時被隱式呼叫- Getter 可用於定義計算屬性,Setter 可用於資料驗證和日誌記錄
- Proxy 是 ES6 新增的物件類型,用於控制對其他物件的存取
- 透過定義 traps 來攔截特定操作(讀取、寫入、函式呼叫等)
- 無法攔截相等比較、
instanceof和typeof
- Proxy 的實用場景包括:日誌記錄、效能測量、資料驗證、自動填充屬性、負數陣列索引
- Proxy 效能開銷大,應謹慎使用並進行效能測試