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> |