Proxy代理对象
前置条件
在开发过程中遇到的一个具有挑战性的需求,涉及到一个独特且复杂的数据结构,这直接关系到如何有效地更新建筑信息
这个项目需求源自于对建筑数据精确管理的必要性,其中建筑数据按照其属性被划分为三种主要类型:多栋建筑、独栋建筑以及局部建筑
多栋建筑是指那些包含若干子建筑单元的复合体,它们共享一套总体的数据——大约10余条重要的信息,并且拥有一个庞大的数组,这个数组详细记录了其中每一个子建筑或局部建筑的详细信息,每个子单元的数据大约包含40至50条具体信息
独栋建筑在数据结构上与多栋建筑中的单个建筑单元类似,同样维护着大约40至50条的详细信息,但它独立存在,不属于任何多栋建筑的一部分
局部建筑则相对较小,包含的数据项约30条,关注点更集中在具体的、局部的部分上
鉴于建筑数据的复杂性和数量众多,我们设计了专门的数据接口来处理查询和更新操作
局部建筑由单一的接口管理,而栋建筑的数据则通过四个不同的接口进行操作,多栋建筑最为复杂,需要六个接口来维护其数据的准确性和时效性
面对这种需求,关键在于如何在用户对某个建筑的数据进行编辑或修改时,能够准确地捕捉到这些变动,并通过正确的接口将更新推送至服务器
为了实现这一目标,我借鉴了Vue3的核心设计理念,采用了Proxy技术
这使得我们能够以一种智能且效率高的方式监听数据的变化,一旦侦测到用户对数据的编辑或更新,就能迅速准确地分析出需要调用的接口类型,进而完成数据的同步更新
为什么选Proxy
在项目开发过程中,我们深入讨论并评估了几种不同的数据更新策略,目的是为了找到最适合我们复杂应用需求的方法
以下是我们考虑的三个方案:
- 方案一:事件监听
描述:通过在模板中对相关节点设置@change事件监听来处理数据更新
评估:此方法虽然直接且易于实现,但当面对复杂交互时,显著增加了HTML编码的工作量及重复性的函数定义,导致代码维护成本上升 - 方案二:Proxy
描述:利用JavaScript的Proxy特性来拦截对象的操作,从而实现数据的响应式更新
评估:Proxy能够提供一套强大的数据监听与拦截机制,但由于其在Vue3项目中的应用相对较新,对于部分开发者而言,学习与应用的门槛相对较高 - 方案三:全量更新
描述:每当数据发生变化时,请求服务器并全面更新页面数据
评估:尽管这种方法实现起来较为简单,但它对服务器资源的消耗过大,在面向大规模用户(如一个省份级别的用户群体)的项目中,这种做法显然是不经济、不实际的
proxy
Proxy 是 ES6 引入的一种机制,它允许我们创建一个对象,该对象可以拦截并自定义基本操作(例如属性查找、赋值、枚举、函数调用等)
Proxy对象的基本概念Proxy对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)Proxy由两个部分组成:- 目标对象:代理将虚拟化该对象的某些操作
- 处理器(handler):一个对象,其属性是当执行一个操作时定义代理行为的函数
使用场景
- 数据验证:可以在对象属性赋值时进行验证
- 属性代理:可以代理对象的属性访问和修改
- 函数代理:可以代理函数的调用
- 观察者模式:可以用来实现观察者模式,监听对象的变化
注意事项
- 性能开销:过度使用
Proxy可能会带来性能开销 - 兼容性:
Proxy在某些旧版本的浏览器中可能不受支持,尤其是 IE 浏览器。因此,在使用Proxy时,确保你的代码运行环境支持它 - 不可代理的对象:某些内置对象和函数不允许被代理,例如
Function.prototype - 陷阱限制:某些陷阱有具体的行为限制。例如,
get陷阱不能返回未定义的属性,而set陷阱必须返回true或false
- 性能开销:过度使用
代理的局限性
虽然
Proxy非常强大,但它也有一些局限性:- 不可代理的内置对象:某些 JavaScript 内置对象无法被代理,如
ArrayBuffer - 性能问题:代理每次操作都会触发陷阱,这可能会导致性能问题,特别是在需要高性能的场景中
- 调试困难:由于代理的动态特性,调试代理对象可能会更加复杂
- 递归代理:如果需要深度代理对象(例如嵌套对象),需要递归地创建代理,这可能会增加代码复杂性
- 不可代理的内置对象:某些 JavaScript 内置对象无法被代理,如
代理的实际用例
- 数据绑定:在前端框架中,
Proxy常用于实现数据绑定。例如 Vue 3 中的响应式系统就是基于Proxy实现的 - 访问控制:可以用
Proxy来控制对对象属性的访问。例如,可以限制某些属性的可读性或可写性 - 日志记录:可以用
Proxy来记录对对象的所有操作,便于调试和监控
- 数据绑定:在前端框架中,
代码实现
定义Proxy的类
1 | <!-- |
注释解释
- 类及构造函数:
ProxyDeep类的构造函数目前没有任何初始化操作,留空即可
initProxy方法:- 参数
obj:需要代理的目标对象或数组handle:代理处理器对象,定义了代理的行为
- 返回值
- 如果目标非法(
null或者非对象类型),返回null - 否则,返回代理后的对象或数组
- 如果目标非法(
- 逻辑
- 首先检查目标是否为对象或数组,如果不是则打印错误信息并返回
null - 定义辅助函数
isObject用于检查值是否为对象(包括数组) - 定义递归函数
traverse,用于遍历目标对象或数组的所有嵌套对象和数组,并将其代理化 - 最后,调用
traverse函数处理传入的对象或数组,并返回代理后的结果
- 首先检查目标是否为对象或数组,如果不是则打印错误信息并返回
- 参数
traverse函数:- 参数
target:当前处理的对象或数组
- 返回值
- 处理后的对象或数组
- 逻辑
- 如果目标是数组,遍历数组的每一项,如果当前项是对象或数组,则递归调用
traverse进行处理 - 如果目标是对象,遍历对象的每一个属性,如果当前属性值是对象或数组,则递归调用
traverse进行处理 - 返回处理后的目标
- 如果目标是数组,遍历数组的每一项,如果当前项是对象或数组,则递归调用
- 参数
为什么数组不做代理
当你代理一个包含数组的对象时,通常来说,你拦截的是对object属性的操作,而不是直接对数组内部元素的操作
如果你的操作目标是一个object的属性,那么只要这个属性的访问或者修改被Proxy拦截,你就能对这个操作进行控制
当代理目标对象中的属性是数组时,实际上你代理的是对这个数组属性的访问和修改,而不是数组内部元素本身
如果直接操作数组(如直接修改数组元素的值、添加新元素等操作),这些操作是不会被属性访问的拦截器(如get、set)捕获的,因为它们不是属性访问操作
1 | const target = { |
如果你将目标对象中的数组也设置为一个代理,并且在这个新的代理中又返回了对原始数组的代理,那么可能会出现递归代理的情况,这可能导致栈溢出错误
这是因为你可能错误地创建了一个无限循环的代理,每次访问数组都会再次调用代理,如此无限循环下去
每次通过代理对象访问array属性时,代码都将创建数组的一个新代理
如果代理的get陷阱返回了另一个代理,那么下次再次访问array属性,就会再次创建新的代理,从而导致递归调用,最终抛出栈溢出错误
定义handle处理对象
此为示例,具体实现可以根据项目需求
1 | const handle = { |
handle
关于 Proxy 的 handler 对象是 Proxy 的核心部分之一
它定义了拦截操作的行为
通过在 handler 对象中定义特定的陷阱(trap),你可以拦截和自定义对象的各种操作行为
基本结构
一个
handler对象是一个包含多个陷阱方法的普通 JavaScript 对象每个陷阱方法对应一个对象操作,例如属性访问、属性设置、函数调用等
常见陷阱方法
示例对象使用
1
2
3
4
5
6
7
8
9// 示例对象
const targetObject = {
a: 1,
b: 2,
c: 3
};
// 创建代理对象
const proxy = new Proxy(targetObject, handle);以下方法都是基于这个例子
get(target, property, receiver)
拦截属性访问操作,例如
proxy.property- 参数
target:被代理的目标对象property:被访问的属性名receiver:代理对象或继承代理对象的对象
- 作用:拦截属性访问操作
- 返回值:返回属性的值
1
2
3
4
5
6
7
8
9
10
11const handle = {
get(target, property, receiver) {
console.log(`Getting property ${property}`);
return Reflect.get(target, property, receiver);
}
}
// 代理对象的操作示例
console.log(proxy.a); // Getting property a- 参数
set(target, property, value, receiver)
拦截属性设置操作,例如
proxy.property = value- 参数
target:被代理的目标对象property:被设置的属性名value:要设置的属性值receiver:代理对象或继承代理对象的对象
- 作用:拦截属性设置操作
- 返回值:必须返回一个布尔值,表示是否成功设置属性
1
2
3
4
5
6
7
8
9
10
11const handle = {
set(target, property, value, receiver) {
console.log(`Setting property ${property} to ${value}`);
return Reflect.set(target, property, value, receiver);
}
}
// 代理对象的操作示例
proxy.b = 10; // Setting property b to 10- 参数
has(target, property)
拦截
in操作符,例如property in proxy- 参数
target:被代理的目标对象property:要检查的属性名
- 作用:拦截
in操作符 - 返回值:返回一个布尔值,表示属性是否存在
1
2
3
4
5
6
7
8
9
10
11const handle = {
has(target, property) {
console.log(`Checking if property ${property} exists`);
return Reflect.has(target, property);
}
}
// 代理对象的操作示例
console.log('b' in proxy); // Checking if property b exists- 参数
deleteProperty(target, property)
拦截
delete操作符,例如delete proxy.property- 参数
target:被代理的目标对象property:要删除的属性名
- 作用:拦截
delete操作符 - 返回值:返回一个布尔值,表示属性是否成功删除
1
2
3
4
5
6
7
8
9
10
11const handle = {
deleteProperty(target, property) {
console.log(`Deleting property ${property}`);
return Reflect.deleteProperty(target, property);
}
}
// 代理对象的操作示例
delete proxy.c; // Deleting property c- 参数
ownKeys(target)
拦截对象自身属性的枚举操作,例如
Object.keys(proxy)- 参数
target:被代理的目标对象
- 作用:拦截对象自身属性的枚举操作,例如
Object.keys(proxy)和for...in循环 - 返回值:返回一个包含目标对象自身属性的数组
1
2
3
4
5
6
7
8
9
10
11const handle = {
ownKeys(target) {
console.log(`Getting own keys of the target`);
return Reflect.ownKeys(target);
}
}
// 代理对象的操作示例
console.log(Object.keys(proxy)); // Getting own keys of the target- 参数
getOwnPropertyDescriptor(target, property)
拦截
Object.getOwnPropertyDescriptor(proxy, property)- 参数
target:被代理的目标对象property:要获取描述符的属性名
- 作用:拦截
Object.getOwnPropertyDescriptor(proxy, property) - 返回值:返回属性描述符对象或
undefined
1
2
3
4
5
6
7
8
9
10
11const handle = {
getOwnPropertyDescriptor(target, property) {
console.log(`Getting descriptor for property ${property}`);
return Reflect.getOwnPropertyDescriptor(target, property);
}
}
// 代理对象的操作示例
console.log(Object.getOwnPropertyDescriptor(proxy, 'a')); // Getting descriptor for property a- 参数
defineProperty(target, property, descriptor)
拦截
Object.defineProperty(proxy, property, descriptor)- 参数
target:被代理的目标对象property:要定义或修改的属性名descriptor:属性描述符对象
- 作用:拦截
Object.defineProperty(proxy, property, descriptor) - 返回值:返回一个布尔值,表示属性是否成功定义或修改
1
2
3
4
5
6
7
8
9
10
11const handle = {
defineProperty(target, property, descriptor) {
console.log(`Defining property ${property} with descriptor`);
return Reflect.defineProperty(target, property, descriptor);
}
}
// 代理对象的操作示例
Object.defineProperty(proxy, 'd', { value: 4, writable: true }); // Defining property d with descriptor- 参数
preventExtensions(target)
拦截
Object.preventExtensions(proxy)- 参数
target:被代理的目标对象
- 作用:拦截
Object.preventExtensions(proxy) - 返回值:返回一个布尔值,表示对象是否成功被标记为不可扩展
1
2
3
4
5
6
7
8
9
10
11const handle = {
preventExtensions(target) {
console.log(`Preventing extensions on the target`);
return Reflect.preventExtensions(target);
}
}
// 代理对象的操作示例
Object.preventExtensions(proxy); // Preventing extensions on the target- 参数
isExtensible(target)
拦截
Object.isExtensible(proxy)- 参数
target:被代理的目标对象
- 作用:拦截
Object.isExtensible(proxy) - 返回值:返回一个布尔值,表示对象是否是可扩展的
1
2
3
4
5
6
7
8
9
10
11const handle = {
isExtensible(target) {
console.log(`Checking if the target is extensible`);
return Reflect.isExtensible(target);
}
}
// 代理对象的操作示例
console.log(Object.isExtensible(proxy)); // Checking if the target is extensible- 参数
getPrototypeOf(target)
拦截
Object.getPrototypeOf(proxy)- 参数
target:被代理的目标对象
- 作用:拦截
Object.getPrototypeOf(proxy) - 返回值:返回目标对象的原型对象
1
2
3
4
5
6
7
8
9
10
11const handle = {
getPrototypeOf(target) {
console.log(`Getting prototype of the target`);
return Reflect.getPrototypeOf(target);
}
}
// 代理对象的操作示例
console.log(Object.getPrototypeOf(proxy)); // Getting prototype of the target- 参数
setPrototypeOf(target, prototype)
拦截
Object.setPrototypeOf(proxy, prototype)- 参数
target:被代理的目标对象prototype:新的原型对象
- 作用:拦截
Object.setPrototypeOf(proxy, prototype) - 返回值:返回一个布尔值,表示原型是否成功设置
1
2
3
4
5
6
7
8
9
10
11const handle = {
setPrototypeOf(target, prototype) {
console.log(`Setting prototype of the target`);
return Reflect.setPrototypeOf(target, prototype);
}
}
// 代理对象的操作示例
Object.setPrototypeOf(proxy, null); // Setting prototype of the target- 参数
apply(target, thisArg, argumentsList)
拦截函数调用,例如
proxy(...args)- 参数
target:被代理的目标函数thisArg:调用函数时的this值argumentsList:调用函数时的参数列表
- 作用:拦截函数调用操作,例如
proxy(...args) - 返回值:返回函数调用的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const handle = {
apply(target, thisArg, argumentsList) {
console.log(`Applying function with arguments ${argumentsList}`);
return Reflect.apply(target, thisArg, argumentsList);
}
}
// 函数代理示例
const targetFunction = function(a, b) {
return a + b;
};
const proxyFunction = new Proxy(targetFunction, handler);
console.log(proxyFunction(1, 2)); // Applying function with arguments 1,2- 参数
construct(target, argumentsList, newTarget)
拦截构造函数调用,例如
new proxy(...args)- 参数
target:被代理的目标构造函数argumentsList:调用构造函数时的参数列表newTarget:创建实例时的构造函数
- 作用:拦截构造函数调用操作,例如
new proxy(...args) - 返回值:返回一个新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const handle = {
construct(target, argumentsList, newTarget) {
console.log(`Constructing new instance with arguments ${argumentsList}`);
return Reflect.construct(target, argumentsList, newTarget);
}
}
// 构造函数代理示例
class TargetClass {
constructor(name) {
this.name = name;
}
}
const proxyClass = new Proxy(TargetClass, handler);
const instance = new proxyClass('Proxy'); // Constructing new instance with arguments Proxy
console.log(instance.name);- 参数
Reflect
Reflect 对象是ES6(ECMAScript 2015)引入的一个内置对象,它提供了一些方法用于操作对象的属性和原型
Reflect 对象的设计目的是为了与 Proxy 对象一起使用,提供更一致和可预测的行为
Reflect 对象的方法与 Object 对象的方法类似,但有一些关键的区别
与 Proxy 的关系
Reflect对象的设计初衷是为了与Proxy对象配合使用Proxy对象可以拦截并自定义基本操作(例如属性查找、赋值、枚举、函数调用等),而Reflect提供了默认的行为实现这样,使用
Reflect可以更容易地在Proxy中调用默认行为一致性和简洁性
Reflect方法的命名和参数顺序与Proxy的捕获器(trap)方法保持一致这种一致性使得代码更易读,也更容易理解和维护
返回值
Reflect方法通常返回布尔值来指示操作是否成功,而不是像某些Object方法那样抛出异常例如,
Reflect.defineProperty在属性定义失败时返回false,而不是抛出异常避免重复代码
在编写
Proxy处理器时,使用Reflect可以避免重复代码例如,在
set捕获器中,可以使用Reflect.set来执行默认的属性赋值操作
方法详解
Reflect.apply(target, thisArgument, argumentsList)描述: 调用一个目标函数,并传入指定的
this值和参数参数
target: 目标函数thisArgument: 函数调用时的this值argumentsList: 一个数组或类数组对象,包含要传递给函数的参数
返回值: 函数调用的结果
示例
1
2
3
4
5function sum(a, b) {
return a + b;
}
const result = Reflect.apply(sum, null, [1, 2]);
console.log(result); // 3
Reflect.construct(target, argumentsList[, newTarget])描述: 等同于使用
new操作符调用一个构造函数参数
target: 目标构造函数argumentsList: 一个数组或类数组对象,包含要传递给构造函数的参数newTarget(可选): 新创建对象的原型。如果未提供,则默认为target
返回值: 新创建的对象
示例
1
2
3
4
5function Person(name) {
this.name = name;
}
const person = Reflect.construct(Person, ['Alice']);
console.log(person.name); // Alice
Reflect.defineProperty(target, propertyKey, attributes)描述: 类似于
Object.defineProperty,定义对象的属性参数
target: 目标对象propertyKey: 属性键attributes: 属性描述符
返回值: 布尔值,表示属性是否成功定义
示例
1
2
3
4const obj = {};
const success = Reflect.defineProperty(obj, 'name', { value: 'Alice' });
console.log(success); // true
console.log(obj.name); // Alice
Reflect.deleteProperty(target, propertyKey)描述: 删除对象的属性
参数
target: 目标对象propertyKey: 属性键
返回值: 布尔值,表示属性是否成功删除
示例
1
2
3
4const obj = { name: 'Alice' };
const success = Reflect.deleteProperty(obj, 'name');
console.log(success); // true
console.log(obj.name); // undefined
Reflect.get(target, propertyKey[, receiver])描述: 获取对象的属性值
参数
target: 目标对象propertyKey: 属性键receiver(可选): 如果是一个代理对象,则作为this值传递给getter函数
返回值: 属性值
示例
1
2
3const obj = { name: 'Alice' };
const value = Reflect.get(obj, 'name');
console.log(value); // Alice
Reflect.getOwnPropertyDescriptor(target, propertyKey)描述: 获取对象自身属性的属性描述符。
参数
target: 目标对象propertyKey: 属性键
返回值: 属性描述符对象或
undefined示例
1
2
3const obj = { name: 'Alice' };
const descriptor = Reflect.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor); // { value: 'Alice', writable: true, enumerable: true, configurable: true }
Reflect.getPrototypeOf(target)描述: 获取对象的原型
参数
target: 目标对象
返回值: 目标对象的原型
示例
1
2
3const obj = {};
const proto = Reflect.getPrototypeOf(obj);
console.log(proto); // Object.prototype
Reflect.has(target, propertyKey)描述: 检查对象是否具有某个属性(包括原型链上的属性)
参数
target: 目标对象propertyKey: 属性键
返回值: 布尔值,表示属性是否存在
示例
1
2
3const obj = { name: 'Alice' };
const hasName = Reflect.has(obj, 'name');
console.log(hasName); // true
Reflect.isExtensible(target)描述: 检查对象是否可扩展(即是否可以添加新属性)
参数
target: 目标对象
返回值: 布尔值,表示对象是否可扩展
示例
1
2
3const obj = {};
const extensible = Reflect.isExtensible(obj);
console.log(extensible); // true
Reflect.ownKeys(target)描述: 获取对象的所有属性键,包括不可枚举属性和符号属性
参数
target: 目标对象
返回值: 包含对象所有属性键的数组
示例
1
2
3const obj = { name: 'Alice' };
const keys = Reflect.ownKeys(obj);
console.log(keys); // ['name']
Reflect.preventExtensions(target)描述: 防止对象扩展(即不允许添加新属性)
参数
target: 目标对象
返回值: 布尔值,表示操作是否成功
示例
1
2
3
4const obj = {};
const success = Reflect.preventExtensions(obj);
console.log(success); // true
console.log(Reflect.isExtensible(obj)); // false
Reflect.set(target, propertyKey, value[, receiver])描述: 设置对象的属性值
参数
target: 目标对象propertyKey: 属性键value: 要设置的值receiver(可选): 如果是一个代理对象,则作为this值传递给setter函数
返回值: 布尔值,表示属性是否成功设置
示例
1
2
3
4const obj = {};
const success = Reflect.set(obj, 'name', 'Alice');
console.log(success); // true
console.log(obj.name); // Alice
Reflect.setPrototypeOf(target, proto)描述: 设置对象的原型
参数
target: 目标对象proto: 新的原型对象
返回值: 布尔值,表示原型是否成功设置
示例
1
2
3
4
5const obj = {};
const proto = { greeting: 'Hello' };
const success = Reflect.setPrototypeOf(obj, proto);
console.log(success); // true
console.log(obj.greeting); // Hello
使用
1 | <template> |









