Object.defineProperty
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。get
属性的 getter 函数,如果没有 getter,则为
undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为undefined。set
属性的
setter函数,如果没有setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为undefined。
因此,可以通过设置属性的 set 方法来实现监听数据变化的效果。
const obj = {
a: 1,
b: [1, 2],
};
for (let key in obj) {
let _val = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log('get ', key);
return _val;
},
set: (value) => {
console.log(`set ${key}: `, value);
_val = value;
},
});
}
obj.a = 2;
console.log(obj.a);
obj.b = [3, 4];
console.log(obj.b);
// set a 2
// get a
// 2
// set b [3, 4]
// get b
// [3, 4]
这样一看没什么问题, 但是当通过数组方法去改变值时,会发现并没有被 set 监听到。
obj.b.push(5);
console.log(obj.b);
// [1, 2, 5] b的值正确没问题,push进去了,但是没有set b
因为我们是调用了数组方法,而不是通过 obj.b = [] 重新赋值,所以解法也简单:重写数组方法,让我们的数组去继承这些方法。
const obj = {
a: 1,
b: [1, 2],
};
const aim = []; // 定义一个工具人数组,下面重写其方法,最后让其他数组的去继承其方法
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
// 重写工具人的方法
arrayMethods.forEach(item => {
Object.defineProperty(aim, item, {
value() {
console.log(`call ${item}`);
[][item].apply(this, arguments);
},
})
});
obj.b.__proto__ = aim; // 让我们的数据继承工具人的方法
obj.b.push(3);
console.log(obj.b);
// call push
// [1, 2, 3]
简单整合后如下
const obj = {
a: 1,
b: [1, 2],
};
const aim = []; // 定义一个数组,下面重写其方法,最后让其他数组的去继承其方法
const arrayMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
];
arrayMethods.forEach(item => {
Object.defineProperty(aim, item, {
value() {
console.log(`call ${item}`);
[][item].apply(this, arguments);
},
})
});
for (let key in obj) {
if (Array.isArray(obj[key])) {
obj[key].__proto__ = aim;
} else {
let _val = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log('get ', key);
return _val;
},
set: (value) => {
console.log(`set ${key}: `, value);
_val = value;
},
});
}
}
obj.a = 2;
console.log(obj.a);
obj.b.push(3);
console.log(obj.b);
Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)target
要使用
Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p的行为。
原理和上面类似,监听属性值变化
const obj = {
a: 1,
b: [1, 2],
};
const objProxy = new Proxy(obj, {
get(target, prop, receiver) {
console.log('get', prop);
return target[prop];
},
set(target, prop, value, receiver) {
console.log('property set: ' + prop + ' = ' + value);
target[prop] = value;
return true;
},
});
objProxy.a = 2;
console.log(objProxy.a);
// property set: a = 2
// get a
// 2
objProxy.b.push(3);
console.log(objProxy.b);
// get b
// get b
// 3
通过上面代码可以看到,属性 a 的监听没问题,但是属性 b 数组的变化还是没有监听到,因为通过这种方式我们监听的是 b 地址值(数组是由一个地址值和地址值指向的堆空间里的数据组成的),我们通过 push 并没有改变地址值,而是改变堆栈中的值,所以没有监听到。解决方法也简单:如果是数组,再次创建一个 Proxy 对象去监听。
const obj = {
a: 1,
b: [1, 2],
};
const fnProxy = (obj) => {
const objProxy = new Proxy(obj, {
get(target, prop, receiver) {
console.log('get', prop);
if (Array.isArray(target[prop])) {
return fnProxy(target[prop]);
}
return target[prop];
},
set(target, prop, value, receiver) {
console.log('property set: ' + prop + ' = ' + value);
target[prop] = value;
return true;
},
});
return objProxy;
};
fnProxy(obj);
const objProxy = fnProxy(obj);
objProxy.b.push(10);
console.log(objProxy.b);
// get b
// get push
// get length
// property set: 2 = 10 这个 ‘2’ 是数组下标
// property set: length = 3
// get b
// [1, 2, 10]
上面的代码中将生成Proxy对象的操作封装了一个方法,如果目标值是数组,就将目标值作为入参再次调用该方法,通过递归的方式完成对数组变化的监听。