Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

JavaScript Proxy 和 Reflect 元編程

Proxy 是 ES2015 加入的,但一直用得不多。Vue 3 的響應式系統基於 Proxy,是時候好好了解一下了。

Proxy 基礎

Proxy 可以攔截對象的各種操作:

javascript
const handler = {
  // 攔截屬性讀取
  get(target, prop, receiver) {
    console.log(`讀取 ${prop}`);
    return Reflect.get(target, prop, receiver);
  },

  // 攔截屬性設置
  set(target, prop, value, receiver) {
    console.log(`設置 ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },

  // 攔截 delete
  deleteProperty(target, prop) {
    console.log(`刪除 ${prop}`);
    return Reflect.deleteProperty(target, prop);
  },

  // 攔截 in 操作符
  has(target, prop) {
    return prop !== "secret" && Reflect.has(target, prop);
  },
};

const obj = { name: "Alice", secret: "密碼" };
const proxy = new Proxy(obj, handler);

proxy.name; // 日誌:讀取 name
proxy.age = 25; // 日誌:設置 age = 25
"secret" in proxy; // false(被攔截了)

實現響應式(Vue 3 原理)

javascript
function reactive(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      // 依賴收集
      track(target, prop);
      const value = Reflect.get(target, prop, receiver);
      // 如果值是對象,遞歸代理
      return typeof value === "object" && value !== null
        ? reactive(value)
        : value;
    },

    set(target, prop, value, receiver) {
      const result = Reflect.set(target, prop, value, receiver);
      // 觸發更新
      trigger(target, prop);
      return result;
    },
  });
}

const state = reactive({ count: 0, user: { name: "Alice" } });

effect(() => {
  console.log("count:", state.count); // 自動追蹤依賴
});

state.count++; // 觸發 effect 重新運行
state.user.name = "Bob"; // 深層屬性也能追蹤!(Object.defineProperty 做不到)

Vue 2 用 Object.defineProperty 不能:

  • 檢測新增屬性(需要 Vue.set
  • 檢測數組索引賦值

Vue 3 用 Proxy 解決了這些問題。

Reflect:標準化對象操作

Reflect 提供了和 Proxy handler 方法對應的靜態方法:

javascript
// 以前的寫法
Object.defineProperty(obj, "name", { value: "Alice" });
"name" in obj;
delete obj.name;

// Reflect 寫法(更統一,返回布爾值表示是否成功)
Reflect.defineProperty(obj, "name", { value: "Alice" });
Reflect.has(obj, "name");
Reflect.deleteProperty(obj, "name");

// Reflect.get 確保 this 正確(receiver 參數)
Reflect.get(target, prop, receiver); // receiver 是 proxy 本身

實用應用:數據驗證

javascript
function createValidator(target, validators) {
  return new Proxy(target, {
    set(target, prop, value) {
      const validator = validators[prop];
      if (validator && !validator(value)) {
        throw new TypeError(`${prop} 的值 "${value}" 不合法`);
      }
      return Reflect.set(target, prop, value);
    },
  });
}

const user = createValidator(
  {},
  {
    age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
    email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
  },
);

user.age = 25; // ✅
user.age = -1; // ❌ TypeError
user.email = "invalid"; // ❌ TypeError

小結

  • Proxy 攔截對象的各種操作(get/set/delete 等),能力比 defineProperty 強
  • Reflect 提供統一的對象操作 API,通常和 Proxy handler 配合使用
  • Vue 3 基於 Proxy 實現響應式,解決了 Vue 2 的新增屬性檢測問題
  • Proxy 無法被 polyfill,只能在支持 ES2015+ 的環境使用

MIT Licensed