js如何在对象的键值变化时调用函数, 并且操作对象的代码不需要变动(无感)
GPT4o给了三种方法, 在后面给出. 第三种方法是改赋值=
为调用函数, 意味着所有的赋值都要改为函数调用,不实用. 分析下方法一Proxy和方法二getter/setter.
Proxy的好处是不会改变原对象,而是建立了一个新的proxy对象, 对新对象的赋值会触发set函数. 问题有三个: 一是任何键值变化都会触发set,需要自己判断键值; 二是需要将原对象的名称指向新proxy对象, 才能实现无感. 三是无法通过instanceof
判断pr一个实例是不是Proxy的实例, 只能自己在创建实例时添加一个自定义key如_isProxy
来判断.(后面段落说明)
getter/setter是最早一代vue.js用的方法, 所有在app.data中的键都会用Object.defineProperty()
的get/set重定义一遍, 做到自动更新UI. 好处是可以逐个key定义,不影响其他key, 问题是原key值就会消失,除非定义前先保存一下. 并且需要保存到另一个键下面(一般前面加个下划线_
)而不能保存在本key下面, 否则会导致反复触发直到调用栈溢出Maximum call stack size exceeded
.
这个话题在vue的官方文档中也有讨论: https://cn.vuejs.org/guide/extras/reactivity-in-depth, 里面也提到在vue3中,从getter/setter已经转向了proxy机制.
另外一个问题, 是如果键值是一个对象(数组也是),也就是下面有子键, 对子键键值的修改, 只会调用get而不是set!并且对Proxy和getter/setter机制都一样. 如:
proxyObject.k = {k1:1} // 调用set
proxyObject.k.k1 = 2 //调用get
怎么解决这个问题,GPT4o的回答是递归代理. 见后面一节.
方法1: 使用Proxy
在 JavaScript 中,如果希望在对象的某个键变化时自动调用一个函数,可以使用 Proxy
对象来实现。Proxy
允许你创建一个代理对象,以便对其属性的访问和修改进行拦截。
下面是一个使用 Proxy
的示例代码,当对象的 someKey
属性变化时,会自动调用函数 a
:
// 定义要监控的对象
const targetObject = {
someKey: '初始值',
};
// 定义要自动调用的函数
function a(newValue) {
console.log(`someKey 的新值是: ${newValue}`);
}
// 创建 Proxy 代理对象
const proxyObject = new Proxy(targetObject, {
set(target, property, value) {
// 当设置 someKey 时,调用函数 a
if (property === 'someKey') {
a(value);
}
// 修改属性的值
target[property] = value;
// 返回 true 表示设置成功
return true;
}
});
// 测试:修改 someKey
proxyObject.someKey = '新的值'; // 这将输出: someKey 的新值是: 新的值
// 可以继续修改其他属性也不会触发 a()
proxyObject.anotherKey = '另一个值';
console.log(proxyObject.anotherKey); // 输出: 另一个值
说明:
- 在上面的示例中,创建了一个对象
targetObject
和一个函数a
。 - 使用
Proxy
创建了代理对象proxyObject
,并在其set
拦截器中定义了逻辑。当someKey
被修改时,会自动调用函数a(newValue)
。 - 修改其他属性不会触发这个函数,只会在指定的键发生变化时执行。
通过这种方式,你可以灵活地监控对象属性的变化,并根据需要触发相应的逻辑。
方法2 使用getter/setter
你可以使用 JavaScript 的 Object.defineProperty()
方法来定义 getter 和 setter,使得每次修改对象属性时都能执行相应的函数。
const targetObject = {
_someKey: '初始值', // 私有变量
};
// 定义要自动调用的函数
function a(newValue) {
console.log(`someKey 的新值是: ${newValue}`);
}
// 使用 Object.defineProperty 定义 getter 和 setter
Object.defineProperty(targetObject, 'someKey', {
get() {
return this._someKey;
},
set(value) {
this._someKey = value;
a(value); // 调用函数 a
},
});
// 测试:修改 someKey
targetObject.someKey = '新的值'; // 这将输出: someKey 的新值是: 新的值
// 读取 someKey
console.log(targetObject.someKey); // 输出: 新的值
// 可以继续修改其他属性也不会触发 a()
targetObject.anotherKey = '另一个值';
console.log(targetObject.anotherKey); // 输出: undefined (因为没有定义)
方法3:使用自定义方法
另一种简单的方法是创建一个用于更新对象属性的方法,以便所有更改都通过此方法发生,这样就可以在里面调用相关逻辑。
const targetObject = {
someKey: '初始值',
};
// 定义要自动调用的函数
function a(newValue) {
console.log(`someKey 的新值是: ${newValue}`);
}
// 自定义方法用于更新键并执行相应逻辑
function setSomeKey(value) {
targetObject.someKey = value; // 更新属性
a(value); // 调用函数 a
}
// 测试:修改 someKey
setSomeKey('新的值'); // 这将输出: someKey 的新值是: 新的值
// 读取 someKey
console.log(targetObject.someKey); // 输出: 新的值
// 修改其他属性的方法不变,不会触发函数 a()
targetObject.anotherKey = '另一个值';
console.log(targetObject.anotherKey); // 输出: 另一个值
如何知道实例是Proxy的实例
在 JavaScript 中,可以使用 Proxy
对象来创建代理,从而控制对某个对象的基本操作,如属性查找、赋值、枚举、函数调用等。如果想要判断一个对象是否是一个 Proxy 实例,可以通过以下方法:
-
利用 WeakMap: 在将对象包装为 Proxy 时,可以使用
WeakMap
来存储原始对象和其代理之间的关系。通过这种方式,你可以检查某个代理是否存在于这个WeakMap
中。const originalObject = {}; const proxyMap = new WeakMap(); const proxy = new Proxy(originalObject, { set(target, prop, value) { return Reflect.set(target, prop, value); } }); // 存入 WeakMap proxyMap.set(proxy, originalObject); function isProxy(obj) { return proxyMap.has(obj); } console.log(isProxy(proxy)); // true console.log(isProxy(originalObject)); // false
-
标记法: 你可以在创建 Proxy 时在其内部设置一个标记属性,以便后续判断。
const originalObject = {}; const proxy = new Proxy(originalObject, { get(target, prop) { if (prop === '_isProxy') return true; return Reflect.get(target, prop); } }); function isProxy(obj) { return obj && obj._isProxy === true; } console.log(isProxy(proxy)); // true console.log(isProxy(originalObject)); // false
-
使用
instanceof
关键字: 不幸的是,直接通过instanceof
来判断是否为 Proxy 是不可能的,因为我们无法直接访问 Proxy 的构造函数。
总的来说,上述方法是较常见的判断一个对象是否为 Proxy 的方案。选择适合自己需求的方法来实现即可。
递归代理
要解决object下的key是对象或者数组, 其子键或者子元素被修改时调用的是get而不是set的问题, GPTo答复是递归代理. 下面给出的是对象的递归代理, 实际还需要考虑数组.
递归代理是对了,但是GPT4o在第一次给出的代码中并没有实现递归代理,指出后,在第二次实现了递归代理, 但设置深层子元素的时候调用了2次set(重复代理了),指出问题后, GPT4o再修改了6次, 也没能回答正确. 感觉是黔驴技穷啦. 这儿贴出其中一次的代码.
function createReactiveObject(obj) {
return new Proxy(obj, {
set(target, property, value) {
// 检查设置的值是否为对象
if (typeof value === 'object' && value !== null) {
// 用 Proxy 处理该对象
value = createReactiveObject(value);
}
// 设置实际值
target[property] = value;
console.log(`属性 ${property} 被设置为: ${value}`); // 输出变更信息
return true; // 返回成功标识
},
get(target, property) {
const value = target[property];
// 仅当获取的是对象时才返回新的代理,避免递归代理产生重复
if (typeof value === 'object' && value !== null) {
return createReactiveObject(value); // 利用递归返回新的代理而不是多次创建同一对象的代理
}
return value;
}
});
}
// 创建动态对象并使用代理函数包裹它
const dynamicObject = createReactiveObject({
k1: { k2: { k3: {} } } // 初始化嵌套结构
});
// 测试:设置嵌套属性
dynamicObject.k1.k2.k3.k4 = 20; // 应只输出一次: 属性 k4 被设置为: 20
// 打印查看当前状态
console.log(dynamicObject);