JS数据劫持 之 Object.defineProperty
1、简介
数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
经典的应用是Vue双向数据绑定。
常见的数据劫持实现方法有两种:
- Object.defineProperty
- proxy设置代理(ES6新增)
本篇文章介绍Object.defineProperty的使用。
2、defineProperty
2.1、介绍
Object.defineProperty(obj, prop, descriptor)
- 参数:
- obj:绑定的目标对象
- prop:绑定的属性名
- descriptor:配置,此参数为一个对象
- descriptor可供定义的选项:
- value:属性的值
- writable:如果为false,属性的值就无法被重写。
- enumerable:是否能在for...in循环中遍历或者在Object.keys中列举出来。
- configurable:configurable默认值为false,如果为false,任何尝试修改目标属性或者修改属性(writable、configurable,enumerable)的行为都会被无效化。
- get:一旦目标属性被访问,就会触发此方法,并将该方法最后的返回值返回给用户。当设置get方法时,不能有value和writable方法,否则会报错。
- set:一旦目标属性被赋值,就会触发此方法。当设置set方法时,不能有value和writable方法,否则会报错。
2.2、代码示例
2.2.1、通过value设置属性值
// 定义空对象
var target = {};
// 通过value赋值
Object.defineProperty(target, 'name', {
value: 'cxk'
})
console.log(target); // {name: 'cxk'}
2.2.2、通过writable设置是否可写/修改
// 定义空对象
var target = {};
target.sex = '女';
Object.defineProperty(target, 'name', {
value: 'cxk',
writable: false, // 不可被重写
});
// 可以添加新的属性
target.age = 18;
console.log(target); // {sex: '女', age: 18, name: 'cxk'}
// 可以重写新的属性
target.age = 20;
console.log(target); // {sex: '女', age: 20, name: 'cxk'}
// 可以重写defineProperty绑定之前的属性
target.sex = '男';
console.log(target); // {sex: '男', age: 20, name: 'cxk'}
// 不可以重写通过defineProperty绑定的属性
target.name = 'wyf'; // Uncaught SyntaxError: Unexpected identifier 'l'
2.2.3、通过enumerable设置属性值是否允许遍历
// 定义空对象
var target = {};
target.sex = '女';
Object.defineProperty(target, 'name', {
value: 'cxk',
enumerable: false, // 不能在for...in循环中遍历或者在Object.keys中列举出来
});
target.age = '18';
// 可以看见,name属性并不能被遍历出来
for (key in target) {
console.log(key); // sex age
console.log(target[key]); // 女 18
}
console.log(Object.keys(target)); // (2) ['sex', 'age']
2.2.4、通过configurable设置属性是否可删
// 定义空对象
var target = {};
target.sex = '女';
Object.defineProperty(target, 'name', {
value: 'cxk',
configurable: true // 这个属性默认为false,也就是不可删除,因此开启测试
});
delete target.name;
console.log(target); // {sex: '女'}
注意点:configurable的默认值为false。
2.2.5、通过get方法获取属性的值
var obj = {
name: 'cxk',
sex: '女',
age: 18
};
// 定义空对象
var target = {};
// 当设置get方法时,不能有value和writable方法,否则会报错。
for (let key in obj) {
Object.defineProperty(target, key, {
get: function() {
return obj[key] + ',练习时长两年半。';
}
});
}
// 修改target对象的sex属性,target和obj都不变
target.sex = '男';
console.log(obj.sex); // 女
console.log(target.sex); // 女,练习时长两年半。
// 修改obj对象的sex属性,target和obj一起变化
obj.sex = '男';
console.log(obj.sex); // 男
console.log(target.sex); // 男,练习时长两年半。
// 对比两个对象的内存地址是否相同,结果为false
console.log(target == obj); // false
// 那可以得出模糊的结论,就是defineProperty方法将二者以某种方式连在了一起。
注意点:
- 当设置get方法时,不能有value和writable方法,否则会报错。
- 访问obj对象,不会触发get方法;访问target对象才会触发get方法。
- target和obj对象通过defineProperty方法连接在一起,两者的内存地址并不相等,但是修改obj的属性,target属性也会跟着变化。
2.2.6、通过set方法设置属性的值
var obj = {
name: 'cxk',
sex: '女',
age: 18
};
// 定义空对象
var target = {};
// 当设置set方法时,不能有value和writable方法,否则会报错。
// 因此,set方法要和get方法一起使用
for (let key in obj) {
Object.defineProperty(target, key, {
get: function() {
return obj[key]
},
set: function(newValue) {
console.log('触发了set方法---值为:' + newValue);
return newValue;
}
});
}
// 修改target
target.sex = '男'; // 触发了set方法---值为:男(但没有修改成功)
console.log(target.sex); // 女
console.log(obj.sex); // 女
// 修改obj
obj.sex = '男'; // 没有触发set方法(但修改成功)
console.log(target.sex); // 男
console.log(obj.sex); // 男
注意点:
- 当设置set方法时,不能有value和writable方法,否则会报错。
- 修改obj对象不会触发set方法;修改target对象才会触发set方法。