Proxy与Reflect

系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)


文章目录

  • 系列文章目录
  • 前言
  • [一、Proxy 是什么](#一、Proxy 是什么)
  • [二、13 种 Trap 全览](#二、13 种 Trap 全览)
    • [2.1 属性读写类(最常用)](#2.1 属性读写类(最常用))
      • [`get(target, prop, receiver)`](#get(target, prop, receiver))
      • [`set(target, prop, value, receiver)`](#set(target, prop, value, receiver))
      • [`has(target, prop)`](#has(target, prop))
    • [2.2 属性描述类](#2.2 属性描述类)
      • [`defineProperty(target, prop, descriptor)`](#defineProperty(target, prop, descriptor))
      • [`getOwnPropertyDescriptor(target, prop)`](#getOwnPropertyDescriptor(target, prop))
      • [`deleteProperty(target, prop)`](#deleteProperty(target, prop))
    • [2.3 遍历与原型类](#2.3 遍历与原型类)
    • [2.4 函数调用类](#2.4 函数调用类)
      • [`apply(target, thisArg, argumentsList)`](#apply(target, thisArg, argumentsList))
      • [`construct(target, argumentsList, newTarget)`](#construct(target, argumentsList, newTarget))
    • [2.5 其他](#2.5 其他)
      • [`isExtensible(target)` / `preventExtensions(target)`](#isExtensible(target) / preventExtensions(target))
  • [三、Reflect:Proxy 的"另一半"](#三、Reflect:Proxy 的"另一半")
    • [3.1 Reflect 的存在意义](#3.1 Reflect 的存在意义)
    • [3.2 receiver 是什么](#3.2 receiver 是什么)
    • [3.3 Reflect 的其他用途](#3.3 Reflect 的其他用途)
  • 四、Proxy.revocable:可撤销的代理
  • [五、Vue 3 为什么选择 Proxy](#五、Vue 3 为什么选择 Proxy)
    • [5.1 Object.defineProperty 的局限(Vue 2)](#5.1 Object.defineProperty 的局限(Vue 2))
    • [5.2 Proxy 的优势(Vue 3)](#5.2 Proxy 的优势(Vue 3))
  • 六、实际应用场景
    • [6.1 数据验证](#6.1 数据验证)
    • [6.2 日志与调试](#6.2 日志与调试)
    • [6.3 负索引数组](#6.3 负索引数组)
    • [6.4 不可变对象(深度冻结)](#6.4 不可变对象(深度冻结))
  • 七、易混淆点
  • 八、思考与练习
  • 总结

前言

上一篇讲了 Symbol 是元编程的入口之一,本篇继续元编程主题:ProxyReflect

如果说 Symbol.iterator 是让对象"告诉"语言如何遍历自己,那 Proxy 就是让对象把所有基本操作都暴露给开发者拦截。读属性、写属性、删除、遍历、函数调用......几乎每一个你对对象能做的事情,Proxy 都能插入自定义逻辑。

而 Reflect,很多人觉得它只是"把 Object 上的方法搬了一遍",但它的真正价值在于和 Proxy 的 trap 一一对应 ,解决 receiver 传递问题。

本篇会讲清楚:Proxy 的 13 种 trap 分别拦截什么?Reflect 存在的意义是什么?为什么 Vue 3 从 Object.defineProperty 切换到了 Proxy?


一、Proxy 是什么

Proxy 可以理解为一个拦截层,包裹在目标对象外面,所有对目标对象的操作都会先经过 Proxy:

复制代码
外部操作 → Proxy(trap 拦截)→ 目标对象

基本语法:

javascript 复制代码
const target = { name: "Alice", age: 25 };
const handler = {
  get(target, prop, receiver) {
    console.log(`读取了 ${prop}`);
    return target[prop];
  }
};

const proxy = new Proxy(target, handler);
proxy.name; // 控制台输出:"读取了 name",返回 "Alice"

关键点

  • new Proxy(target, handler) 创建代理对象
  • target 是被代理的原始对象
  • handler 是一个普通对象,其属性名是 trap 名,属性值是拦截函数
  • 不定义某个 trap 时,操作直接穿透到 target,没有额外开销

二、13 种 Trap 全览

Proxy 支持 13 种 trap,覆盖了对象的基本操作。我按使用频率和重要性分组讲解。

2.1 属性读写类(最常用)

get(target, prop, receiver)

拦截属性读取proxy.fooproxy["foo"]

javascript 复制代码
const handler = {
  get(target, prop) {
    if (prop === "age") {
      return target[prop] < 0 ? 0 : target[prop];
    }
    return Reflect.get(target, prop);
  }
};

const person = new Proxy({ age: -5 }, handler);
console.log(person.age); // 0(拦截修正了负数)

set(target, prop, value, receiver)

拦截属性赋值proxy.foo = 1

javascript 复制代码
const handler = {
  set(target, prop, value) {
    if (prop === "age" && (typeof value !== "number" || value < 0)) {
      throw new TypeError("age 必须是非负数");
    }
    target[prop] = value;
    return true; // 必须返回 true,否则严格模式下抛 TypeError
  }
};

const person = new Proxy({}, handler);
person.age = 25;    // 正常
// person.age = -1; // TypeError: age 必须是非负数

面试重点:set 必须返回 true 。在严格模式下,如果 set trap 返回 falsy 值,会抛出 TypeError。这是因为规范要求 [[Set]] 内部方法返回一个布尔值表示是否成功。

has(target, prop)

拦截 in 操作符:"foo" in proxy

javascript 复制代码
const handler = {
  has(target, prop) {
    if (prop.startsWith("_")) {
      return false; // 下划线开头的属性"隐藏"
    }
    return prop in target;
  }
};

const obj = new Proxy({ _secret: 42, name: "Alice" }, handler);
console.log("_secret" in obj); // false
console.log("name" in obj);    // true

注意:has 只拦截 in 操作符,不影响 Object.keys()for...in

2.2 属性描述类

defineProperty(target, prop, descriptor)

拦截 Object.defineProperty()proxy.prop = value(后者也会触发)。

getOwnPropertyDescriptor(target, prop)

拦截 Object.getOwnPropertyDescriptor()

deleteProperty(target, prop)

拦截 delete proxy.prop

javascript 复制代码
const handler = {
  deleteProperty(target, prop) {
    if (prop === "id") {
      throw new Error("id 属性不可删除");
    }
    delete target[prop];
    return true; // 必须返回 true,否则严格模式报错
  }
};

const obj = new Proxy({ id: 1, name: "test" }, handler);
delete obj.name; // 正常
// delete obj.id; // Error: id 属性不可删除

2.3 遍历与原型类

ownKeys(target)

拦截以下操作:

  • Object.getOwnPropertyNames(proxy)
  • Object.getOwnPropertySymbols(proxy)
  • Object.keys(proxy)
  • for...in 循环
javascript 复制代码
const handler = {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith("_"));
  }
};

const obj = new Proxy({ _internal: 1, visible: 2 }, handler);
console.log(Object.keys(obj)); // ["visible"]

注意:ownKeys 必须返回一个数组,且不能包含目标对象上不存在的属性,否则会报错。

getPrototypeOf(target)

拦截 Object.getPrototypeOf(proxy)proxy instanceof Constructor 的原型查找。

setPrototypeOf(target, proto)

拦截 Object.setPrototypeOf(proxy, proto)

2.4 函数调用类

apply(target, thisArg, argumentsList)

拦截函数调用proxy()。只能用于函数对象。

javascript 复制代码
function sum(a, b) { return a + b; }

const handler = {
  apply(target, thisArg, args) {
    console.log(`调用参数:${args}`);
    const result = target.apply(thisArg, args);
    console.log(`返回结果:${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, handler);
proxiedSum(1, 2); // "调用参数:1,2" → "返回结果:3"

construct(target, argumentsList, newTarget)

拦截 new 操作符:new proxy()。只能用于构造函数。

javascript 复制代码
class User {
  constructor(name) { this.name = name; }
}

const handler = {
  construct(target, args, newTarget) {
    console.log(`创建实例:${args}`);
    return new target(...args);
  }
};

const ProxiedUser = new Proxy(User, handler);
new ProxiedUser("Alice"); // "创建实例:Alice"

2.5 其他

isExtensible(target) / preventExtensions(target)

拦截 Object.isExtensible()Object.preventExtensions()


三、Reflect:Proxy 的"另一半"

3.1 Reflect 的存在意义

Reflect 是 ES6 新增的内置对象 ,它的方法和 Proxy 的 trap 一一对应。这不是巧合,而是有意设计。

Reflect 解决的核心问题是:在 Proxy trap 中,如何正确地调用默认行为?

错误示范

javascript 复制代码
const handler = {
  get(target, prop, receiver) {
    // 直接用 target[prop] 有问题!
    return target[prop];
  }
};

const parent = new Proxy({ x: 1 }, handler);
const child = Object.create(parent);
child.x; // 如果 x 是 getter,this 会指向 parent 而不是 child

正确写法

javascript 复制代码
const handler = {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
};

Reflect.get(target, prop, receiver) 会把 receiver 正确传递下去,确保 this 指向正确。

3.2 receiver 是什么

receiver 是 Proxy 链中的"最终调用者"。考虑这个场景:

javascript 复制代码
const parent = {
  get greet() {
    return `Hello, I'm ${this.name}`;
  }
};

const child = Object.create(parent);
child.name = "Child";

console.log(child.greet); // "Hello, I'm Child"

this 正确指向 child,因为 JavaScript 内部用 receiver 来传递"谁在调用"。

但如果用 Proxy:

javascript 复制代码
const handler = {
  get(target, prop, receiver) {
    // 错误:return target[prop];
    // 正确:
    return Reflect.get(target, prop, receiver);
  }
};

const proxiedParent = new Proxy(parent, handler);
const child = Object.create(proxiedParent);
child.name = "Child";

console.log(child.greet); // 正确:"Hello, I'm Child"

如果用 target[prop] 代替 Reflect.getthis 会指向 target(原始 parent),而不是 child

3.3 Reflect 的其他用途

除了和 Proxy 配合,Reflect 还有一些实用价值:

javascript 复制代码
// 1. 替代 Object 上的一些方法(返回布尔值,更规范)
Reflect.defineProperty(obj, prop, descriptor); // 返回 true/false
Reflect.deleteProperty(obj, prop);             // 返回 true/false

// 2. 替代 delete 操作符(函数形式,更明确)
delete obj.prop;            // 旧写法
Reflect.deleteProperty(obj, prop); // Reflect 写法

// 3. 替代 in 操作符
Reflect.has(obj, prop); // 等价于 prop in obj

// 4. 安全地调用 Object 方法(避免对象不是对象时的异常)
Reflect.apply(fn, thisArg, args); // 替代 fn.apply(thisArg, args)

一句话总结 :Reflect 是操作对象的函数式 API,语义更清晰,错误处理更规范。


四、Proxy.revocable:可撤销的代理

new Proxy() 创建的代理是不可撤销的。如果你需要在某些场景下禁用代理 ,用 Proxy.revocable

javascript 复制代码
const target = { secret: 42 };
const { proxy, revoke } = Proxy.revocable(target, {
  get(target, prop) {
    return target[prop];
  }
});

console.log(proxy.secret); // 42

revoke(); // 撤销代理

// 之后任何操作都会报错
// proxy.secret; // TypeError: Cannot perform 'get' on a proxy that has been revoked
// proxy.foo = 1; // TypeError

应用场景

  1. 一次性访问令牌:授权后立即撤销
  2. 安全沙箱:代码执行完毕后禁用对敏感对象的访问
  3. 缓存失效:当缓存过期时,撤销代理以强制重新获取
javascript 复制代码
function createSecureAPI(data) {
  const { proxy, revoke } = Proxy.revocable(data, {
    get(target, prop) {
      if (prop === "revoke") return revoke;
      return Reflect.get(target, prop);
    }
  });
  return proxy;
}

const api = createSecureAPI({ getUser: () => ({ name: "Alice" }) });
api.getUser(); // { name: "Alice" }
api.revoke();  // 撤销
// api.getUser(); // TypeError

五、Vue 3 为什么选择 Proxy

这是面试高频问题,理解 Proxy 相比 Object.defineProperty 的优势是关键。

5.1 Object.defineProperty 的局限(Vue 2)

javascript 复制代码
const obj = {};
Object.defineProperty(obj, "name", {
  get() { console.log("读取 name"); return this._name; },
  set(val) { console.log("设置 name"); this._name = val; }
});

obj.name = "Alice"; // "设置 name"
obj.name;           // "读取 name"

问题 1:无法监听新增属性

javascript 复制代码
const state = { a: 1 };
// 只劫持了 a
Object.defineProperty(state, "b", { /* ... */ }); // 需要手动添加

// Vue 2 必须用 $set
this.$set(this.obj, "newProp", 123);

问题 2:无法监听数组索引和长度

javascript 复制代码
const arr = [1, 2, 3];
arr[0] = 10;     // 不会触发 setter
arr.length = 0;  // 不会触发 setter

// Vue 2 只能重写数组方法(push, pop, splice 等)

问题 3:需要递归遍历所有属性

javascript 复制代码
// Vue 2 初始化时要遍历整个 data 对象
function observe(obj) {
  for (const key in obj) {
    defineReactive(obj, key, obj[key]);
    if (typeof obj[key] === "object") {
      observe(obj[key]); // 递归
    }
  }
}

5.2 Proxy 的优势(Vue 3)

javascript 复制代码
const state = new Proxy({ a: 1 }, {
  get(target, prop, receiver) {
    track(target, prop); // 依赖收集
    return Reflect.get(target, prop, receiver);
  },
  set(target, prop, value, receiver) {
    const result = Reflect.set(target, prop, value, receiver);
    trigger(target, prop); // 触发更新
    return result;
  }
});

state.b = 2;     // 自动触发响应式
state.arr[0] = 1; // 自动触发响应式

优势总结

对比项 Object.defineProperty Proxy
新增属性 需要 $set 自动监听
数组索引 需要重写方法 自动监听
删除属性 无法监听 deleteProperty trap
性能 初始化递归遍历 惰性代理(访问时才代理子对象)
代码复杂度 需要重写数组方法 统一 trap 处理

惰性代理是关键优化:

javascript 复制代码
const state = new Proxy({ nested: { deep: { value: 1 } } }, {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);
    if (typeof value === "object" && value !== null) {
      // 只有访问时才代理子对象
      return new Proxy(value, this);
    }
    return value;
  }
});

// 只有访问 nested 时才会代理 nested
// 访问 nested.deep 时才会代理 deep
state.nested.deep.value; // 代理链按需创建

六、实际应用场景

6.1 数据验证

javascript 复制代码
function createValidated(schema) {
  return new Proxy({}, {
    set(target, prop, value) {
      const validate = schema[prop];
      if (validate && !validate(value)) {
        throw new TypeError(`${prop} 验证失败`);
      }
      return Reflect.set(target, prop, value);
    }
  });
}

const user = createValidated({
  age: v => typeof v === "number" && v >= 0 && v <= 150,
  email: v => typeof v === "string" && v.includes("@")
});

user.age = 25;    // 正常
// user.age = -1; // TypeError
// user.email = "invalid"; // TypeError

6.2 日志与调试

javascript 复制代码
function createLogger(obj, name = "Object") {
  return new Proxy(obj, {
    get(target, prop, receiver) {
      console.log(`[${name}] get ${String(prop)}`);
      return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
      console.log(`[${name}] set ${String(prop)} = ${value}`);
      return Reflect.set(target, prop, value, receiver);
    },
    deleteProperty(target, prop) {
      console.log(`[${name}] delete ${String(prop)}`);
      return Reflect.deleteProperty(target, prop);
    }
  });
}

const state = createLogger({ count: 0 }, "Counter");
state.count++; // "[Counter] get count" → "[Counter] set count = 1"

6.3 负索引数组

javascript 复制代码
function createArray(arr) {
  return new Proxy(arr, {
    get(target, prop, receiver) {
      if (typeof prop === "string" && /^-?\d+$/.test(prop)) {
        const index = Number(prop);
        if (index < 0) {
          return Reflect.get(target, target.length + index, receiver);
        }
      }
      return Reflect.get(target, prop, receiver);
    }
  });
}

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

6.4 不可变对象(深度冻结)

javascript 复制代码
function deepFreeze(obj) {
  return new Proxy(obj, {
    set() { throw new Error("只读对象"); },
    deleteProperty() { throw new Error("只读对象"); },
    defineProperty() { throw new Error("只读对象"); },
    setPrototypeOf() { throw new Error("只读对象"); },
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver);
      if (typeof value === "object" && value !== null) {
        return deepFreeze(value); // 递归代理
      }
      return value;
    }
  });
}

const frozen = deepFreeze({ nested: { value: 1 } });
// frozen.nested.value = 2; // Error: 只读对象
// frozen.nested = {};      // Error: 只读对象

七、易混淆点

  1. Proxy 不是透明的typeof proxy === "object",但 proxy === targetfalse。Proxy 是一个新对象,不是 target 本身。
  2. set trap 必须返回 true:否则严格模式下抛 TypeError。这是规范要求,不是可选的。
  3. has 不拦截 Object.keys()has 只拦截 in 操作符。遍历用 ownKeys
  4. getreceiver 不总是 proxy :当通过原型链访问时,receiver 是实际发起访问的对象(可能是子对象)。
  5. Proxy 不能代理原始值new Proxy(1, handler) 会报错,Proxy 只能代理对象。
  6. Reflect 和 Object 方法的区别:Reflect 方法返回布尔值表示成功/失败,Object 方法可能抛错或返回对象本身。
  7. Proxy 是浅层的 :默认情况下,嵌套对象不会自动被代理,需要在 get trap 中递归创建。

八、思考与练习

1. 以下代码输出什么?为什么?

javascript 复制代码
const target = {};
const proxy = new Proxy(target, {});
proxy.a = 1;
console.log(target.a);

解析:输出 1。没有定义 trap 时,操作直接穿透到 target。proxy 和 target 共享同一个底层对象。

2. 为什么 Proxy 不能代理 MapSet 等内置对象?

解析:MapSet 的内部方法(如 [[MapData]])依赖 this 指向原始对象。直接代理会导致 this 指向 proxy,内部方法报错。需要特殊处理:

javascript 复制代码
const map = new Map();
const proxy = new Proxy(map, {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);
    if (typeof value === "function") {
      return value.bind(target); // 绑定到原始对象
    }
    return value;
  }
});

3. Vue 3 的 reactive() 是如何用 Proxy 实现的?为什么需要 tracktrigger

解析:trackget 中收集依赖(记录哪些组件用到了这个属性),triggerset 中触发更新(通知用到这个属性的组件重新渲染)。这是响应式系统的核心。

4. Proxy.revocable 和直接用 Proxy 有什么区别?

解析:Proxy.revocable 返回 { proxy, revoke },可以调用 revoke() 永久禁用代理。普通 Proxy 一旦创建就无法撤销。

5. 如何用 Proxy 实现一个简单的 JSON.stringify 替代品,只序列化可枚举的自有属性?

javascript 复制代码
function safeStringify(obj) {
  const proxy = new Proxy(obj, {
    ownKeys(target) {
      return Object.keys(target); // 只返回可枚举自有属性
    }
  });
  return JSON.stringify(proxy);
}

6. 为什么 Reflect.get(target, prop, receiver)target[prop] 更安全?

解析:当 prop 是 getter 时,target[prop]this 指向 targetReflect.get 会把 receiver 传递给 getter,确保 this 指向正确的对象(可能是 proxy 或子对象)。


总结

  • Proxy 是对象操作的拦截层,13 种 trap 覆盖几乎所有基本操作。
  • Reflect 和 Proxy trap 一一对应,解决 receiver 传递问题,是操作对象的规范 API。
  • Vue 3 选择 Proxy 是因为它能自动监听新增属性、数组索引、删除操作,且支持惰性代理优化性能。
  • Proxy 的核心价值是元编程:让开发者能透明地拦截和自定义对象行为,而不改变原始代码。
  • set trap 必须返回 trueget trap 中用 Reflect.get 传递 receiver 是最佳实践。

下一篇讲 Map / Set / WeakMap / WeakSet:键类型、顺序、弱引用集合与 DOM 元数据关联(系列第 33 篇,大纲 §33)。

相关推荐
小蜜蜂dry2 小时前
nestjs实战-权限二:角色模块
前端·后端·nestjs
rm1092 小时前
【js逆向】webpack自吐算法记录
javascript
AskHarries2 小时前
权限模型:Shell、Browser、文件读写的安全边界
服务器·前端·网络
小蜜蜂dry2 小时前
nestjs实战-权限一: 菜单模块
前端·后端·nestjs
用户5812441541572 小时前
GemDesign MCP协议详解:从原型到代码的完整技术链路
前端
半个烧饼不加肉2 小时前
JS 底层探究-- 事件循环
开发语言·前端·javascript
goDeep2 小时前
useMemo 和 useCallback 的区别,我终于搞懂了
前端
小亮学前端2 小时前
在1Panel中部署Nuxt项目
前端·vue.js
产品研究员2 小时前
AI生成可用的React交互代码实测:Lovable vs Stitch vs Paico
前端·react.js·aigc