Proxy与Reflect
ES6中的Proxy与Reflect
JavaScript的Proxy(代理)和Reflect(反射)是ES6引入的特性,它们共同为对象操作提供了便捷。
一、Proxy:对象操作的拦截器
Proxy本质上是一个"包装器",它可以包裹任何对象(包括数组、函数甚至另一个Proxy),并在原对象的操作执行前插入自定义逻辑。红宝书中将proxy类比c++中的指针(只是帮助理解,实际存在很大区别),可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作,但直接操作会绕过代理施予的行为。
1. Proxy的基本使用
在代理对象上执行的任何操作实际上都会应用到目标对象
const target = { id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError
console.log(proxy instanceof Proxy); // TypeError// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
2. Proxy的捕获器使用
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的
拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接
或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对
象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = { name: "张三" };
const handler = {// 拦截器方法get(target, propKey, receiver) {console.log(`读取属性: ${propKey}`);return target[propKey];}
};
const proxy = new Proxy(target, handler);// 访问代理对象会触发拦截
console.log(proxy.name); // 输出"读取属性: name"和"张三"
- target:被代理的原始对象,Proxy不会修改原对象本身
- handler:拦截器配置对象,每个属性对应一种操作的拦截逻辑
- proxy:代理实例,所有对proxy的操作都会先经过handler处理
3. 关键拦截器
get拦截器
在 JavaScript 代码中可以通过多种形式触发并被 get()捕获器拦截到。proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在
代理对象上,就会触发 get()捕获器。
const target = { foo: 'bar'
};
const handler = { get() { return 'handler override'; }
};
const proxy = new Proxy(target, handler); console.log(target.foo); // bar
console.log(proxy.foo); // handler override
console.log(target['foo']); // bar
console.log(proxy['foo']); // handler override
console.log(Object.create(target)['foo']); // bar
console.log(Object.create(proxy)['foo']); // handler override
get拦截器不仅处理常规属性访问,还需要处理数组索引:
const arr = [1, 2, 3];
const arrProxy = new Proxy(arr, {get(target, propKey, receiver) {// 处理数组索引if (typeof propKey === 'string' && /^\d+$/.test(propKey)) {console.log(`访问数组索引: ${propKey}`);}// 处理数组方法if (['push', 'pop'].includes(propKey)) {console.log(`调用数组方法: ${propKey}`);}return Reflect.get(target, propKey, receiver);}
});arrProxy[0]; // 输出"访问数组索引: 0"
arrProxy.push(4); // 输出"调用数组方法: push"
set拦截器的返回值意义
set
拦截器需要返回布尔值,表示设置操作是否成功,严格模式下返回false
会抛出TypeError:
'use strict';
const user = { age: 20 };
const userProxy = new Proxy(user, {set(target, propKey, value) {if (propKey === 'age' && (value < 0 || value > 150)) {console.error('年龄必须在0-150之间');return false; // 设置失败}return Reflect.set(target, propKey, value);}
});userProxy.age = 200; // 抛出TypeError: 'set' on proxy: trap returned falsish for property 'age'
4. 可撤销代理
有时候可能需要中断代理对象与目标对象之间的联系。对于使用 new Proxy()创建的普通代理来
说,这种联系会在代理对象的生命周期内一直持续存在。
Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的
操作是不可逆的。撤销代理之后再调用代理会抛出 TypeError。
const target = { foo: 'bar'
};
const handler = { get() { return 'intercepted'; }
};
//在实例化的时候同时获得撤销函数和代理对象
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke(); //撤销代理
console.log(proxy.foo); // TypeError
5.捕获器不变式
根据 ECMAScript 规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
const target = {};
Object.defineProperty(target, 'foo', { configurable: false, writable: false, value: 'bar'
});
const handler = { get() { return 'qux'; }
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo);
二、Reflect:对象操作的标准化工具
Reflect是一个内置对象,它提供了一组与Proxy拦截器对应的静态方法,旨在规范化JavaScript的对象操作。
1. Reflect的设计理念
- 函数式接口:将原本属于运算符的操作(如
in
、delete
)转换为函数调用 - 统一返回值:操作成功返回
true
,失败返回false
(与Proxy拦截器的返回值要求一致) - 错误处理:操作失败时返回
false
而非抛出异常,简化错误处理 - this绑定:所有方法都接受
receiver
参数,正确处理this指向
2. Reflect与Object方法的对比
操作 | Object方法 | Reflect方法 | 区别 |
---|---|---|---|
获取属性 | obj[prop] | Reflect.get(obj, prop) | Reflect可指定receiver |
设置属性 | obj[prop] = value | Reflect.set(obj, prop, value) | Reflect返回操作结果 |
属性检查 | prop in obj | Reflect.has(obj, prop) | 函数式调用 |
删除属性 | delete obj[prop] | Reflect.deleteProperty(obj, prop) | Reflect返回操作结果 |
定义属性 | Object.defineProperty() | Reflect.defineProperty() | Reflect返回布尔值 |
3. Reflect在Proxy中的关键作用
在Proxy拦截器中,使用Reflect而非直接操作target有三个重要原因:
- 正确维护this指向:
const obj = {name: "张三",getSelf() {return this;}
};// 不使用Reflect的情况
const badProxy = new Proxy(obj, {get(target, propKey) {return target[propKey]; // this会指向target而非proxy}
});// 使用Reflect的情况
const goodProxy = new Proxy(obj, {get(target, propKey, receiver) {return Reflect.get(target, propKey, receiver); // this指向proxy}
});console.log(badProxy.getSelf() === badProxy); // false
console.log(goodProxy.getSelf() === goodProxy); // true
-
传播操作状态:Reflect方法的返回值正好满足Proxy拦截器对返回值的要求
-
保持默认行为:确保在自定义逻辑之外,对象行为与原生保持一致