第 15 章

Proxy 与 Reflect:13个陷阱与元编程完整实现

Proxy 是 JavaScript 有史以来最强大的元编程工具。它允许你在任何对象操作的底层安插钩子——不只是属性读写,还包括 in 运算符、deletenew、函数调用、原型访问等全部13种操作。Reflect 是它的镜像:每个 Proxy 陷阱都有一个 Reflect 方法提供该操作的默认行为。Vue 3 的整个响应式系统建立在这两个 API 之上。

🔹 Level 1 · 你需要知道的

Proxy 的基本结构

const proxy = new Proxy(target, handler);
const target = { name: 'Alice', age: 30 };

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 → 'Alice'
proxy.name = 'Bob'; // 输出: 设置属性: name = Bob

最常用的4个陷阱

get 陷阱(拦截属性读取):

// Vue 3 响应式系统的简化版
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key); // 收集依赖
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 触发更新
      return result;
    }
  });
}

has 陷阱(拦截 in 运算符):

// 实现"范围"语义:5 in range(1, 10)
function range(min, max) {
  return new Proxy({}, {
    has(target, prop) {
      const num = Number(prop);
      return !isNaN(num) && num >= min && num <= max;
    }
  });
}

const r = range(1, 10);
console.log(5 in r);  // true
console.log(15 in r); // false
console.log('a' in r); // false

deleteProperty 陷阱(拦截 delete 运算符):

// 实现只读保护
const readOnly = new Proxy({ x: 1 }, {
  deleteProperty(target, prop) {
    throw new Error(`禁止删除属性: ${prop}`);
  },
  set(target, prop, value) {
    throw new Error(`禁止修改属性: ${prop}`);
  }
});

apply 陷阱(拦截函数调用):

// 函数调用日志
function logged(fn) {
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      console.log(`调用 ${fn.name}(${args.join(', ')})`);
      const result = Reflect.apply(target, thisArg, args);
      console.log(`返回: ${result}`);
      return result;
    }
  });
}

const add = logged((a, b) => a + b);
add(3, 4); // 输出: 调用 (3, 4) → 返回: 7

Reflect 的用途

Reflect 提供的每个方法都对应一个 Proxy 陷阱,调用它可以执行该操作的默认行为:

// 在陷阱中调用 Reflect 转发默认行为
const handler = {
  get(target, key, receiver) {
    // 自定义逻辑...
    return Reflect.get(target, key, receiver); // 转发
  }
};

// Reflect 与传统 Object 方法的关键区别:
// Reflect.defineProperty 返回 boolean(不抛错)
// Object.defineProperty 返回对象或抛 TypeError

Reflect.defineProperty({}, 'x', { value: 1 }); // true(成功)
Reflect.defineProperty(Object.freeze({}), 'x', { value: 1 }); // false(失败,不抛错)

5个常见错误

错误1:直接在陷阱中操作 target 而不用 Reflect

// 危险:会导致 this 绑定错误
const handler = {
  get(target, key, receiver) {
    return target[key]; // 错误!用 target 而不是 receiver
  }
};

// 考虑这种情况:
const proto = new Proxy({}, {
  get(target, key, receiver) {
    return target[key]; // 如果子对象访问继承属性,this 会指向 proto 而不是 child
  }
});

const child = Object.create(proto);
child.name = 'child';
// 如果 proto 的 getter 使用 target[key],this 绑定可能错误

// 正确做法:
const handler2 = {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver); // receiver 正确传递
  }
};

错误2:get 陷阱返回了错误的值(违反不变量)

const target = {};
Object.defineProperty(target, 'x', { value: 42, writable: false, configurable: false });

const proxy = new Proxy(target, {
  get(target, key) {
    return 99; // 违反不变量!
  }
});

proxy.x; // TypeError: 'get' on proxy: property 'x' is a read-only and non-configurable...
// 规范要求:对 writable:false, configurable:false 的属性,get 陷阱必须返回实际值

错误3:set 陷阱忘记返回 true

const proxy = new Proxy({}, {
  set(target, key, value) {
    target[key] = value;
    // 忘记 return true!
  }
});

proxy.x = 1; // 严格模式下报 TypeError: 'set' on proxy returned false

错误4:代理 Proxy 对象(嵌套代理触发两次)

const inner = new Proxy({}, { get(t, k) { console.log('inner get', k); return t[k]; } });
const outer = new Proxy(inner, { get(t, k) { console.log('outer get', k); return t[k]; } });

outer.x;
// 输出:outer get x(outer 拦截)
//       inner get x(outer 用 t[k] 触发了 inner 的陷阱!)
// 如果用 Reflect.get(t, k, receiver),只会触发一次

错误5:Proxy.revocable 撤销后继续使用

const { proxy, revoke } = Proxy.revocable({ x: 1 }, {});
proxy.x; // 1
revoke();
proxy.x; // TypeError: Cannot perform 'get' on a proxy that has been revoked

🔸 Level 2 · 它是怎么运行的

全部13个陷阱

陷阱 拦截的内部方法 触发时机
get [[Get]] 属性读取、原型链查找
set [[Set]] 属性赋值
has [[HasProperty]] in 运算符
deleteProperty [[Delete]] delete 运算符
apply [[Call]] 函数调用 fn()
construct [[Construct]] new 运算符
getPrototypeOf [[GetPrototypeOf]] Object.getPrototypeOfinstanceof__proto__
setPrototypeOf [[SetPrototypeOf]] Object.setPrototypeOf__proto__ =
isExtensible [[IsExtensible]] Object.isExtensible
preventExtensions [[PreventExtensions]] Object.preventExtensions/seal/freeze
getOwnPropertyDescriptor [[GetOwnProperty]] Object.getOwnPropertyDescriptor
defineProperty [[DefineOwnProperty]] Object.defineProperty、属性创建
ownKeys [[OwnPropertyKeys]] Object.keys/values/entriesfor...inObject.getOwnPropertyNames/Symbols

用 Proxy 实现 Vue 3 响应式核心(精简版)

以下是一个功能完整的 Vue 3 响应式核心实现,约40行:

// 全局存储:WeakMap<object, Map<key, Set<effect>>>
const targetMap = new WeakMap();
let activeEffect = null; // 当前正在执行的 effect

// 追踪依赖
function track(target, key) {
  if (!activeEffect) return;

  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));

  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));

  dep.add(activeEffect);
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const effects = depsMap.get(key);
  if (effects) effects.forEach(effect => effect());
}

// 创建响应式对象
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);
      track(target, key); // 读取时收集依赖
      // 如果值是对象,递归代理
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }
      return value;
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 写入时触发更新
      return result;
    }
  });
}

// 注册 effect(副作用函数)
function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    fn(); // 执行时收集依赖
    activeEffect = null;
  };
  effectFn(); // 立即执行一次(触发依赖收集)
  return effectFn;
}

// 使用示例:
const state = reactive({ count: 0, name: 'Vue' });

effect(() => {
  console.log(`count is ${state.count}`);
  // 执行时读取 count → track(state, 'count') → 把这个 effect 添加到依赖
});

state.count++; // trigger(state, 'count') → 重新执行 effect → 输出 "count is 1"
state.name = 'Vue 3'; // trigger(state, 'name') → 没有订阅 name 的 effect,无输出

不变量(Invariants)系统

规范对每个 Proxy 陷阱的返回值都有约束,违反约束会抛出 TypeError。这是安全机制:

各陷阱的关键不变量:

get 陷阱:
  如果 target 上该属性是 writable:false, configurable:false
  → get 必须返回与 target 上实际值相同的值

set 陷阱:
  如果 target 上该属性是 writable:false, configurable:false
  → set 不能成功(必须返回 false,否则 TypeError)

has 陷阱:
  如果 target 上该属性是 configurable:false
  → has 必须返回 true(不能假装属性不存在)
  如果 target 不可扩展,且属性不存在
  → has 必须返回 false

deleteProperty 陷阱:
  如果 target 上该属性是 configurable:false
  → deleteProperty 不能返回 true(不能假装删除成功)

ownKeys 陷阱:
  如果 target 不可扩展
  → ownKeys 必须包含 target 上所有自有属性键,不能包含额外的
  所有 configurable:false 的属性键都必须出现在结果中

getPrototypeOf 陷阱:
  如果 target 不可扩展
  → getPrototypeOf 必须返回与 target 实际原型相同的值

isExtensible 陷阱:
  返回值必须与 target 的实际 extensible 状态相同
  (这是不变量中最严格的——不能撒谎)

🔺 Level 3 · 规范怎么定义的

规范 §10.5:Proxy Object Internal Methods

规范 §10.5.1 是 Proxy [[Get]] 的定义,代表了所有陷阱的典型结构:

Proxy [[Get]] (P, Receiver)(§10.5.8):

1. handler ← O.[[ProxyHandler]]
   如果 handler 是 null → TypeError(代理已被撤销)

2. target ← O.[[ProxyTarget]]

3. trap ← GetMethod(handler, "get")
   如果 trap 是 undefined → 返回 target.[[Get]](P, Receiver)
   (没有陷阱 → 使用默认行为)

4. trapResult ← Call(trap, handler, [target, P, Receiver])
   (调用陷阱函数)

5. 不变量检查(ValidateNonRevokedProxy 等):
   targetDesc ← target.[[GetOwnProperty]](P)
   如果 targetDesc 不是 undefined:
     如果 IsDataDescriptor(targetDesc):
       如果 targetDesc.[[Configurable]] 是 false
          且 targetDesc.[[Writable]] 是 false:
         如果 SameValue(trapResult, targetDesc.[[Value]]) 是 false
           → TypeError(不能对不可配置、不可写属性返回不同的值)
     如果 IsAccessorDescriptor(targetDesc):
       如果 targetDesc.[[Configurable]] 是 false
          且 targetDesc.[[Get]] 是 undefined:
         如果 trapResult 不是 undefined
           → TypeError(get 为 undefined 的访问器属性不能返回非 undefined)

6. 返回 trapResult

ownKeys 陷阱的精确不变量

ownKeys 陷阱的不变量是所有陷阱中最复杂的(§10.5.11):

规范约束(Proxy [[OwnPropertyKeys]]):

设 trapResultKeys = 陷阱返回的键列表
设 targetKeys = target.[[OwnPropertyKeys]]()

约束1:trapResultKeys 中没有重复键
约束2:trapResultKeys 的每个元素必须是 String 或 Symbol

如果 target 不可扩展:
  约束3:trapResultKeys 必须包含 targetKeys 的所有元素
  约束4:targetKeys 的每个元素必须出现在 trapResultKeys 中且只出现一次

如果 target 可扩展:
  约束5:targetKeys 中所有 configurable:false 的属性必须出现在 trapResultKeys 中
// 验证不变量违反:
const target = Object.freeze({ x: 1, y: 2 }); // 不可扩展

const proxy = new Proxy(target, {
  ownKeys() {
    return ['x']; // 缺少 'y' — 违反约束3
  }
});

Object.keys(proxy); // TypeError: 'ownKeys' on proxy: trap result did not include 'y'

// 正确的 ownKeys 用法(过滤可枚举属性):
const proxy2 = new Proxy({ a: 1, _b: 2, c: 3 }, {
  ownKeys(target) {
    // 只暴露不以 _ 开头的键
    return Reflect.ownKeys(target).filter(k => !String(k).startsWith('_'));
  },
  getOwnPropertyDescriptor(target, key) {
    // 必须同步修改 getOwnPropertyDescriptor,否则 Object.keys 会把这些键过滤掉
    if (!String(key).startsWith('_')) {
      return { ...Object.getOwnPropertyDescriptor(target, key), enumerable: true };
    }
    return undefined;
  }
});

Object.keys(proxy2); // ['a', 'c']

defineProperty 陷阱的不变量

规范约束(Proxy [[DefineOwnProperty]]):

约束1:如果 target 不可扩展,陷阱不能返回 true 来添加不存在的属性

约束2:如果 target 上属性是 configurable:false,
  陷阱不能返回 true 来修改不允许的字段:
  - 不能把 configurable 改为 true
  - 不能修改 enumerable
  - 如果 writable:false,不能修改 value(除非 SameValue 相同)
  - 不能把 writable:false 改为 true

约束3:如果 target 上属性是 configurable:false 且 writable:false,
  不能以任何方式修改

💎 Level 4 · 边界与陷阱

陷阱1:Proxy 不能代理原始值

new Proxy(42, {});     // TypeError: Cannot create proxy with a non-object as target
new Proxy(null, {});   // TypeError
new Proxy('str', {});  // TypeError
new Proxy(true, {});   // TypeError

// 只能代理对象(包括函数):
new Proxy({}, {});         // OK
new Proxy([], {});         // OK
new Proxy(function(){}, {}); // OK
new Proxy(class {}, {});   // OK

// 为什么?Proxy 拦截的是对象的内部方法([[Get]]、[[Set]] 等),
// 原始值没有内部方法,无法代理

陷阱2:get 陷阱不变量的精确触发条件

这是最容易触发 TypeError 的陷阱,完整推导:

const target = {};
Object.defineProperty(target, 'frozen', {
  value: 42,
  writable: false,
  configurable: false
});
Object.defineProperty(target, 'accessorFrozen', {
  get: undefined, // get 是 undefined
  configurable: false
});

const proxy = new Proxy(target, {
  get(target, key) {
    if (key === 'frozen') return 99;        // 违反不变量!
    if (key === 'accessorFrozen') return 1; // 违反不变量!(getter 是 undefined,必须返回 undefined)
    return target[key];
  }
});

proxy.frozen;         // TypeError: 不能对不可写、不可配置属性返回不同的值
proxy.accessorFrozen; // TypeError: 访问器的 [[Get]] 为 undefined 时必须返回 undefined

// 正确处理:
const proxy2 = new Proxy(target, {
  get(target, key, receiver) {
    // Reflect.get 自动处理不变量
    return Reflect.get(target, key, receiver);
  }
});
proxy2.frozen; // 42 — 正确

陷阱3:Proxy 套 Proxy,陷阱触发两次

let getCount = 0;

const inner = new Proxy({ x: 1 }, {
  get(target, key, receiver) {
    getCount++;
    console.log(`inner get #${getCount}: ${key}`);
    return Reflect.get(target, key, receiver);
  }
});

const outer = new Proxy(inner, {
  get(target, key, receiver) {
    getCount++;
    console.log(`outer get #${getCount}: ${key}`);
    return target[key]; // 错误!target 是 inner,target[key] 会触发 inner 的 get 陷阱
  }
});

outer.x;
// 输出:
// outer get #1: x
// inner get #2: x

// 如果用 Reflect.get(target, key, receiver) 替代 target[key]:
// 同样会触发两次!因为 receiver 是 outer,Reflect.get 仍然转发到 inner

// 要避免触发两次,在 outer 陷阱中直接操作 innerTarget:
const innerTarget = { x: 1 };
const outerProxy = new Proxy(new Proxy(innerTarget, innerHandler), {
  get(outerTarget, key, receiver) {
    // 如果需要,直接读 innerTarget(绕过 inner 代理)
    // 但通常嵌套代理就是为了两层都触发陷阱
  }
});

嵌套代理的合法使用场景

// 外层:日志
// 内层:验证
// 两层都应该触发是合理的

const validated = new Proxy({ x: 1 }, {
  set(target, key, value) {
    if (typeof value !== 'number') throw new TypeError('Only numbers allowed');
    return Reflect.set(target, key, value);
  }
});

const logged = new Proxy(validated, {
  set(target, key, value, receiver) {
    console.log(`Setting ${key} = ${value}`);
    return Reflect.set(target, key, value, receiver); // 传播到 validated 的 set
  }
});

logged.x = 2;    // 输出日志 + 通过验证
logged.x = 'a'; // 输出日志 + 验证抛出 TypeError

陷阱4:Proxy.revocable 详解

const { proxy, revoke } = Proxy.revocable({ x: 1, y: 2 }, {
  get(target, key) {
    return Reflect.get(target, key);
  }
});

// 使用代理
console.log(proxy.x); // 1

// 撤销代理
revoke();

// 任何操作都会 TypeError:
proxy.x;            // TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy.x = 1;        // TypeError
delete proxy.x;     // TypeError
'x' in proxy;       // TypeError
Object.keys(proxy); // TypeError

// revoke 可以多次调用(幂等):
revoke(); // 不报错

// 使用场景:临时权限
function withTemporaryAccess(sensitiveObj, operation) {
  const { proxy, revoke } = Proxy.revocable(sensitiveObj, {});
  try {
    return operation(proxy);
  } finally {
    revoke(); // 无论如何都撤销访问权限
  }
}

const secret = { token: 'secret-value' };
withTemporaryAccess(secret, (p) => {
  console.log(p.token); // 'secret-value'
  // operation 完成后,代理自动撤销
});

陷阱5:Reflect 与 Object 方法的精确区别

操作 Reflect 版本 Object 版本 关键区别
定义属性 Reflect.defineProperty(obj, key, desc) → boolean Object.defineProperty(obj, key, desc) → obj 或 TypeError Reflect 失败时返回 false,Object 失败时抛错
删除属性 Reflect.deleteProperty(obj, key) → boolean delete obj.key → boolean(非严格模式)或 TypeError(严格模式 + configurable:false) Reflect 更一致
检查属性 Reflect.has(obj, key) → boolean key in obj → boolean 功能相同
获取原型 Reflect.getPrototypeOf(obj) Object.getPrototypeOf(obj) 对非对象:Reflect 抛 TypeError,Object 在新规范中也抛 TypeError
阻止扩展 Reflect.preventExtensions(obj) → boolean Object.preventExtensions(obj) → obj 返回类型不同
获取自有属性键 Reflect.ownKeys(obj) Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj)) Reflect.ownKeys 包含所有键(String + Symbol),且按规范顺序
// 重要:Reflect.defineProperty vs Object.defineProperty 的错误处理
const frozen = Object.freeze({ x: 1 });

// Object 版:抛错
try {
  Object.defineProperty(frozen, 'y', { value: 2 });
} catch (e) {
  console.log('Object.defineProperty threw:', e.message);
}

// Reflect 版:返回 false
const success = Reflect.defineProperty(frozen, 'y', { value: 2 });
console.log('Reflect.defineProperty returned:', success); // false

// 在 Proxy 陷阱中,Reflect 版更适合:
const proxy = new Proxy({}, {
  defineProperty(target, key, desc) {
    // 用 Reflect 版,失败时返回 false,Proxy 层会正确处理
    return Reflect.defineProperty(target, key, desc);
    // 如果用 Object.defineProperty,异常可能跑到错误的调用栈
  }
});

陷阱6:用 Proxy 实现"不可能的"功能

负数组索引(Python 风格的 arr[-1]):

function negativeable(arr) {
  return new Proxy(arr, {
    get(target, key, receiver) {
      const index = Number(key);
      if (!isNaN(index) && index < 0) {
        key = String(target.length + index);
      }
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const index = Number(key);
      if (!isNaN(index) && index < 0) {
        key = String(target.length + index);
      }
      return Reflect.set(target, key, value, receiver);
    }
  });
}

const arr = negativeable([1, 2, 3, 4, 5]);
console.log(arr[-1]); // 5
console.log(arr[-2]); // 4
arr[-1] = 99;
console.log(arr);     // [1, 2, 3, 4, 99]

深层只读对象(真正递归的只读):

function deepReadOnly(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver);
      // 递归代理对象
      if (typeof value === 'object' && value !== null) {
        return deepReadOnly(value);
      }
      return value;
    },
    set(target, key, value) {
      throw new TypeError(`Cannot set property '${String(key)}' on read-only object`);
    },
    deleteProperty(target, key) {
      throw new TypeError(`Cannot delete property '${String(key)}' on read-only object`);
    }
  });
}

const config = deepReadOnly({
  database: { host: 'localhost', port: 5432 },
  app: { port: 3000 }
});

config.database.host;         // 'localhost' — 可读
config.database.host = 'new'; // TypeError
config.database.port = 9999;  // TypeError
delete config.app.port;       // TypeError

自动填充默认值(自动创建嵌套路径):

function autoDefault(obj = {}) {
  return new Proxy(obj, {
    get(target, key) {
      if (!(key in target)) {
        target[key] = autoDefault(); // 自动创建子对象
      }
      return target[key];
    }
  });
}

const config = autoDefault();
config.database.host = 'localhost'; // 自动创建 config.database
config.database.port = 5432;
config.app.server.port = 3000;      // 自动创建 config.app, config.app.server

console.log(config.database.host);  // 'localhost'
console.log(config.app.server.port); // 3000

// 注意:这个实现会创建对象链,但不适合用于判断属性是否"真正"存在

类型安全的属性访问(运行时类型检查):

function typed(obj, schema) {
  return new Proxy(obj, {
    set(target, key, value) {
      if (key in schema) {
        const expectedType = schema[key];
        if (typeof value !== expectedType) {
          throw new TypeError(
            `Property '${key}' must be ${expectedType}, got ${typeof value}`
          );
        }
      }
      return Reflect.set(target, key, value);
    }
  });
}

const user = typed({}, {
  name: 'string',
  age: 'number',
  active: 'boolean'
});

user.name = 'Alice';  // OK
user.age = 30;        // OK
user.age = '30';      // TypeError: Property 'age' must be number, got string

本章小结

  1. Proxy 拦截13个对象操作,覆盖所有内部方法:从属性读写(get/set)到原型访问(getPrototypeOf),从键列举(ownKeys)到函数调用(apply/construct)。每个陷阱对应一个内部方法。

  2. Reflect 是每个 Proxy 陷阱的默认实现:在陷阱中调用 Reflect.xxx(target, ...) 转发默认行为,同时正确传递 receiver(this 值)。关键区别:Reflect 方法返回 boolean,对应的 Object 方法返回对象或抛错。

  3. 不变量是规范对陷阱的硬性约束:无论陷阱写了什么,引擎都会验证返回值。对不可写、不可配置的属性,get 陷阱必须返回实际值;对 isExtensible 陷阱,必须返回与 target 一致的值。

  4. 嵌套 Proxy 会使陷阱触发多次:外层代理访问内层代理时,如果用 target[key] 而不是操作底层 target,内层陷阱也会触发。Proxy.revocable 可创建可撤销代理,撤销后任何操作都报 TypeError。

  5. Proxy 开启了真正的元编程能力:负数组索引、深层只读、自动默认值、类型运行时检查——这些在没有 Proxy 时需要大量样板代码才能实现的功能,现在可以在十几行内完成。Vue 3 的整个响应式系统正是建立在 get + set 陷阱上。

本章评分
4.8  / 5  (19 评分)

💬 留言讨论