Featured image of post JavaScript 代理程式與 Reflect API:元程式模式Featured image of post JavaScript 代理程式與 Reflect API:元程式模式

JavaScript 代理程式與 Reflect API:元程式模式

探索用於元程式設計的 JavaScript 代理程式和 Reflect API。學習 Vue 3 和 MobX 中的 13 個陷阱、Reflect 方法、驗證、虛擬屬性和現實世界模式。

JavaScript 的 ProxyReflect API 是該語言中最強大的元程式設計工具之一。它們允許開發人員攔截和自訂物件的基本操作——屬性存取、賦值、枚舉、函數調用,甚至建構函數調用。本文探討了所有 13 個代理陷阱、互補的 Reflect API 以及 Vue 3、MobX 和 Immer 等生產框架中使用的真實模式。

了解代理模式

Proxy 包裝目標物件並透過稱為陷阱的處理函數攔截操作:

const target = { name: "Alice", age: 30 };
const handler = {
  get(target, prop, receiver) {
    console.log(`Getting property "${prop}"`);
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Logs: Getting property "name" → "Alice"

目標不變。處理程序定義如何轉送、修改或阻止代理程式上的操作。代理與包裝函數或裝飾器不同,因為它在語言層級進行攔截 - indeletefor...innew 都可以自訂。


13 代理陷阱

Proxy 提供了 13 個陷阱,涵蓋了每個基本的物件操作。最常用的是getset,它們攔截屬性的讀寫。 以下是完整的參考:

陷阱何時觸發論點
佔位符_0物業閱讀佔位符_1
佔位符_0屬性寫佔位符_1
佔位符_0in 運算子佔位符_2
佔位符_0delete 運算子佔位符_2
佔位符_0Object.keys()for...in佔位符_3
佔位符_0函數呼叫佔位符_1
佔位符_0new 關鍵字佔位符_2
佔位符_0佔位符_1佔位符_2
佔位符_0佔位符_1佔位符_2
佔位符_0佔位符_1佔位符_2
佔位符_0佔位符_1佔位符_2
佔位符_0佔位符_1佔位符_2
佔位符_0佔位符_1佔位符_2

每個陷阱都必須強制執行不變量。例如,get 陷阱不能傳回與不可寫、不可配置屬性的實際值不同的值。


反射 API

Reflect 方法鏡像每個代理陷阱並提供預設轉送行為。在陷阱內使用 Reflect 可確保 receiver 參數的正確傳播,這對於繼承屬性和 getter 中正確的 this 綁定至關重要:

const parent = {
  get fullName() { return `${this.first} ${this.last}`; }
};
const handler = {
  get(target, prop, receiver) {
    // Without Reflect.get, the getter's 'this' would be wrong
    return Reflect.get(target, prop, receiver);
  }
};
const proxy = new Proxy(Object.create(parent), handler);
proxy.first = "John";
proxy.last = "Doe";
console.log(proxy.fullName); // "John Doe"

思考模型很簡單:ReflectProxy 的關係就像 Object 與直接屬性存取一樣。始終透過 Reflect 轉送陷阱,以在新增自訂邏輯時保持預設行為。


驗證和資料完整性

set 陷阱可以對物件強制實施架構約束:

const validator = {
  set(target, prop, value) {
    if (prop === "age" && (!Number.isInteger(value) || value < 0)) {
      throw new TypeError("Age must be a positive integer");
    }
    if (prop === "email" && !/^[^\s@]+@[^\s@]+$/.test(value)) {
      throw new TypeError("Invalid email format");
    }
    return Reflect.set(target, prop, value);
  }
};
const user = new Proxy({}, validator);
user.age = 25;  // OK
user.age = -5;  // TypeError: Age must be a positive integer

只讀代理會阻止所有突變陷阱:

const readOnly = target =>
  new Proxy(target, {
    set: () => { throw new Error("Object is read-only"); },
    deleteProperty: () => { throw new Error("Object is read-only"); },
    defineProperty: () => { throw new Error("Object is read-only"); },
  });

虛擬屬性和計算值

Proxy 可以透過在 get 陷阱中動態計算目標物件上不存在的屬性來公開這些屬性:

const range = (from, to) =>
  new Proxy({ from, to }, {
    get(target, prop) {
      if (prop === "size") return target.to - target.from + 1;
      if (prop === "contains") return (v) => v >= target.from && v <= target.to;
      return Reflect.get(target, prop);
    }
  });

const r = range(1, 10);
console.log(r.size);      // 10
console.log(r.contains(5)); // true

這種惰性求值模式可以透過 WeakMap 進行快取擴展,以避免重新計算。


財產觀察和變化跟踪

使用 set 陷阱的簡單可觀察:

function observe(target, onChange) {
  return new Proxy(target, {
    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      onChange(prop, value);
      return result;
    }
  });
}

為了進行深度觀察,請使用 WeakMap 緩存遞歸地包裝嵌套對象,以避免雙重包裝。此技術是 Vue 3 的 reactive() 系統的基礎,它取代了 Vue 2 的基於 Object.defineProperty 的方法。


可撤銷代理

Proxy.revocable 建立一個可以永久停用的代理:

const { proxy, revoke } = Proxy.revocable(target, handler);
// use proxy normally...
revoke();
// any subsequent operation throws TypeError

這對於授予對敏感資源的臨時存取權限和執行清理合約非常有用。


生產模式:Vue 3、MobX、Immer

Vue 3 在其 reactive() 系統中使用 Proxy。 get 陷阱追蹤屬性存取以建立依賴關係圖,而 set 觸發重新渲染。 MobX 6+ 同樣將 defineProperty 替換為 observable() 的代理程式。 Immer.js 使用 Proxy 產生具有寫入時複製語意的不可變草稿 — set 標記修改,而 get 在存取時建立巢狀草稿。


性能考慮因素

每個被捕獲的操作都會產生開銷。避免在緊密循環中代理熱路徑物件。選擇性地應用代理程式——「代理內部,而不是外部」——並在優化之前進行分析。具有大型 WeakMap 快取的深度代理也會影響記憶體使用。

代理是驗證、反應性和虛擬資源的正確工具。對於效能關鍵路徑,請考慮更簡單的模式是否足夠。如果應用得當,Proxy 可以實現任何其他 JavaScript 功能不可能或繁瑣的元程式模式。