跳至主要內容

JavaScript Proxy 和 Reflect:元程式設計的強大利器

6 分鐘閱讀 1,000 字

JavaScript Proxy 和 Reflect:元程式設計的強大利器

ProxyReflect 是 ES6 引入的兩個強大 API,讓你能夠攔截並自訂幾乎所有的物件操作。雖然日常開發中不常直接使用它們,但許多流行的框架和庫(Vue 3 的響應式系統、Immer.js、MobX)都在底層大量使用這兩個 API。

Proxy 基本概念

Proxy 讓你建立一個「代理」物件,攔截對目標物件的各種操作:

const target = { name: "Alice", age: 25 };

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);
  }
};

const proxy = new Proxy(target, handler);

proxy.name;        // 輸出:讀取屬性:name
proxy.age = 26;    // 輸出:設定屬性:age = 26

可攔截的操作(Trap)

Proxy 支援 13 種 trap,涵蓋所有物件操作:

Trap 攔截的操作
`get` 讀取屬性 `obj.prop`
`set` 設定屬性 `obj.prop = val`
`has` `in` 運算子
`deleteProperty` `delete obj.prop`
`apply` 函式呼叫
`construct` `new` 運算子
`getOwnPropertyDescriptor` `Object.getOwnPropertyDescriptor()`
`ownKeys` `Object.keys()`, `for...in`
`defineProperty` `Object.defineProperty()`
`getPrototypeOf` `Object.getPrototypeOf()`
`setPrototypeOf` `Object.setPrototypeOf()`
`isExtensible` `Object.isExtensible()`
`preventExtensions` `Object.preventExtensions()`

實用範例一:資料驗證

function createValidatedObject(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      if (prop in schema) {
        const validator = schema[prop];
        if (!validator(value)) {
          throw new TypeError(
            `屬性 "${prop}" 的值 "${value}" 不符合驗證規則`
          );
        }
      }
      target[prop] = value;
      return true;
    }
  });
}

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

user.name = "Bob";           // OK
user.age = 25;               // OK
user.email = "not-an-email"; // 拋出 TypeError

實用範例二:不可變物件(類似 Immer)

function makeImmutable(obj) {
  return new Proxy(obj, {
    set(target, prop, value) {
      throw new TypeError(`不能修改屬性 "${prop}",物件是不可變的`);
    },
    deleteProperty(target, prop) {
      throw new TypeError(`不能刪除屬性 "${prop}",物件是不可變的`);
    },
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      // 遞迴使嵌套物件也不可變
      if (typeof value === 'object' && value !== null) {
        return makeImmutable(value);
      }
      return value;
    }
  });
}

const config = makeImmutable({
  db: { host: 'localhost', port: 5432 },
  cache: { ttl: 3600 }
});

config.db.host = 'other'; // TypeError

實用範例三:自動預設值

// 存取不存在的屬性時,回傳預設值或自動建立
function withDefaults(target, defaults = {}) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      }
      if (prop in defaults) {
        return defaults[prop];
      }
      // 未定義的屬性回傳 null 而非 undefined
      return null;
    }
  });
}

const settings = withDefaults(
  { theme: 'dark' },
  { language: 'zh-TW', timezone: 'Asia/Taipei', notifications: true }
);

console.log(settings.theme);          // 'dark'
console.log(settings.language);       // 'zh-TW'(來自 defaults)
console.log(settings.unknown);        // null

實用範例四:追蹤物件變更

function trackChanges(obj) {
  const changes = [];

  const proxy = new Proxy(obj, {
    set(target, prop, value, receiver) {
      changes.push({
        type: 'set',
        prop,
        oldValue: target[prop],
        newValue: value,
        timestamp: Date.now()
      });
      return Reflect.set(target, prop, value, receiver);
    },
    deleteProperty(target, prop) {
      changes.push({
        type: 'delete',
        prop,
        oldValue: target[prop],
        timestamp: Date.now()
      });
      return Reflect.deleteProperty(target, prop);
    }
  });

  return { proxy, getChanges: () => [...changes] };
}

const { proxy: user, getChanges } = trackChanges({ name: 'Alice', age: 25 });
user.name = 'Alicia';
user.age = 26;
delete user.age;

console.log(getChanges());
// [
//   { type: 'set', prop: 'name', oldValue: 'Alice', newValue: 'Alicia', ... },
//   { type: 'set', prop: 'age', oldValue: 25, newValue: 26, ... },
//   { type: 'delete', prop: 'age', oldValue: 26, ... }
// ]

Reflect API

ReflectProxy 的最佳夥伴,提供與 Proxy trap 一一對應的靜態方法:

// 為什麼要用 Reflect 而非直接操作?

// 問題一:this 綁定錯誤
const handler = {
  get(target, prop, receiver) {
    // 錯誤:直接存取可能破壞繼承鏈上的 getter
    // return target[prop];

    // 正確:Reflect.get 保留正確的 receiver
    return Reflect.get(target, prop, receiver);
  }
};

// 問題二:set 必須回傳 boolean
const handler2 = {
  set(target, prop, value, receiver) {
    // 錯誤:忘記回傳 true 會在 strict mode 拋出 TypeError
    // target[prop] = value;

    // 正確:Reflect.set 回傳 boolean
    return Reflect.set(target, prop, value, receiver);
  }
};

攔截函式呼叫(apply trap)

function memoize(fn) {
  const cache = new Map();

  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const key = JSON.stringify(args);

      if (cache.has(key)) {
        console.log(`快取命中:${key}`);
        return cache.get(key);
      }

      const result = Reflect.apply(target, thisArg, args);
      cache.set(key, result);
      return result;
    }
  });
}

const expensiveCalc = memoize((n) => {
  console.log(`計算中...`);
  return n * n;
});

expensiveCalc(5);  // 計算中... → 25
expensiveCalc(5);  // 快取命中 → 25
expensiveCalc(6);  // 計算中... → 36

效能考量

Proxy 比直接物件操作慢,使用前要評估:

  • 讀寫頻繁的熱路徑(hot path)應避免使用
  • 建立大量 Proxy 實例會增加 GC 壓力
  • 在效能敏感場景,考慮只在開發環境啟用 Proxy(如驗證邏輯)

小結

Proxy 和 Reflect 開啟了 JavaScript 元程式設計的大門。資料驗證、響應式系統、不可變資料結構、ORM 映射——許多複雜的功能都能用這兩個 API 優雅地實現。Vue 3 的整個響應式系統就建立在 Proxy 之上,理解它們能讓你更深刻地理解現代框架的運作原理。

分享這篇文章