Proxy 讓你包裹一個物件,攔截並自訂對它的基本操作(讀取、寫入、刪除、判斷屬性是否存在等)。這類「對程式行為本身進行程式設計」的能力,稱為元程式設計(Metaprogramming)。

Reflect 則是一組與 Proxy 攔截器一一對應的內建方法,用來在攔截器內「執行原本預設的操作」。兩者通常搭配使用。

基本概念#

new Proxy(target, handler) 接收兩個參數:被代理的目標物件,以及定義各種攔截器(trap)的處理器物件。

const target = { name: "Andrew" };

const proxy = new Proxy(target, {
  get(obj, prop) {
    console.log(`讀取 ${prop}`);
    return obj[prop];
  },
  set(obj, prop, value) {
    console.log(`設定 ${prop} = ${value}`);
    obj[prop] = value;
    return true; // set trap 必須回傳布林表示成功
  },
});

proxy.name; // log: 讀取 name → "Andrew"
proxy.age = 30; // log: 設定 age = 30

常見攔截器#

Trap攔截的操作觸發範例
get讀取屬性proxy.x
set寫入屬性proxy.x = 1
hasin 運算子'x' in proxy
deletePropertydeletedelete proxy.x
apply函式呼叫proxy()
constructnew 呼叫new proxy()

搭配 Reflect#

在攔截器內若要「先做點事、再執行預設行為」,用 Reflect 比直接操作 obj[prop] 更安全——它能正確處理 this 與回傳值,且方法簽名與 trap 完全對應。

const proxy = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log(`讀取 ${prop}`);
    return Reflect.get(obj, prop, receiver); // 等同預設讀取行為
  },
  set(obj, prop, value, receiver) {
    return Reflect.set(obj, prop, value, receiver); // 回傳布林
  },
});

原則:攔截器負責「加邏輯」,Reflect 負責「做原本該做的事」。 這樣攔截器只需專注於要插入的行為,預設語義交給 Reflect 保證正確。

實用範例#

屬性驗證#

const user = new Proxy(
  {},
  {
    set(obj, prop, value) {
      if (prop === "age" && (typeof value !== "number" || value < 0)) {
        throw new TypeError("age 必須是非負數");
      }
      return Reflect.set(obj, prop, value);
    },
  }
);

user.age = 30; // OK
user.age = -5; // TypeError

預設值與防呆讀取#

// 讀取不存在的屬性時回傳預設值,而非 undefined
const withDefault = new Proxy(
  { theme: "dark" },
  {
    get(obj, prop) {
      return prop in obj ? obj[prop] : "(未設定)";
    },
  }
);

withDefault.theme; // "dark"
withDefault.lang; // "(未設定)"

負索引陣列#

function negativeArray(arr) {
  return new Proxy(arr, {
    get(obj, prop) {
      const idx = Number(prop);
      if (idx < 0) return obj[obj.length + idx];
      return Reflect.get(obj, prop);
    },
  });
}

const arr = negativeArray(["a", "b", "c"]);
arr[-1]; // "c"
arr[-2]; // "b"

Proxy vs getter/setter#

在 Proxy 出現前,攔截屬性存取要靠物件的 get/set 存取器,但那只能針對「已知的特定屬性」。Proxy 的強項是能攔截任意屬性,甚至是尚未存在的屬性。

比較項getter/setterProxy
攔截範圍預先定義的特定屬性所有屬性,含動態新增的
攔截操作讀 / 寫讀寫、indelete、呼叫…
動態屬性做不到天然支援
效能接近原生明顯較慢

Proxy 有顯著的效能開銷。 每次經過攔截器都比直接存取慢上一個量級,且有些操作(=== 比較、typeofinstanceof)無法被攔截。

在熱路徑(如動畫迴圈、大量資料處理)上使用前,務必實測;許多響應式框架(如 Vue 3)正是用 Proxy 實現響應式,但會搭配快取與批次更新來攤平成本。

小結#

  • Proxy 攔截物件的基本操作,是元程式設計的核心工具。
  • Reflect 提供與攔截器對應的「預設行為」方法,搭配使用最穩妥。
  • 適合做驗證、預設值、日誌、響應式等橫切邏輯。
  • 代價是效能與部分操作無法攔截,熱路徑慎用。