虽然说背八股的时候嘎嘎猛,但是一想下来还没仔细研究过definProperty和Proxy这两个方法,于是便有了这个篇文章。
虽然研究了,但好像也不是很仔细...
废话不多说!
Object.defineProperty
用于在一个对象上定义一个新属性,或者修改一个已存在的属性,并返回该对象。
主要作用是为对象的属性提供更精细的控制,例如可写,可枚举,可配置等。
语法
Object.defineProperty(obj, prop, descriptor)、
参数说明
obj
目标对象,即需要定义或修改属性的对象。prop
要定义或修改的属性名称(字符串或 Symbol)。descriptor
属性描述符,是一个对象,用于定义或修改属性的行为。它包含以下可选字段:
-
数据描述符 :
value: 属性的值,默认为undefined。writable: 是否可以修改属性的值,默认为false。enumerable: 是否可以通过for...in或Object.keys()枚举,默认为false。configurable: 是否可以删除该属性或修改其描述符,默认为false。
-
存取描述符 ):
get: 一个函数,用作属性的 getter,默认为undefined。set: 一个函数,用作属性的 setter,默认为undefined。
示例
1. 定义只读属性
const obj={}
Object.defineProperty(obj,'name',{
value:'张三',
writable:false,//是否可写
enumerable:true,//是否可枚举
configurable:true//是否可以删除该属性或修改其描述符
})//定义属性
console.log(obj.name);// 张三
obj.name='李四'
console.log(obj.name)// 张三
2. 使用存取描述符(getter 和 setter)
const obj={
_name:'张三'
}
Object.defineProperty(obj,'name',{
get(){
console.log(222);
return this._name
},
set(newName){
this._name=newName
console.log(111);
},
enumerable:true,
configurable:true
})
console.log(obj.name);// 222,张三 |触发getter
obj.name='李四'// 111 |触发setter
console.log(obj.name)// 222,李四 |触发getter
3. 创建不可枚举属性
const obj={
age:12
}
Object.defineProperty(obj,'secret',{
value:[1,2,3],
enumerable:false
})
console.log(obj.secret);// [ 1, 2, 3 ]
console.log(Object.keys(obj));// [ 'age' ] |secret不可被枚举
4. 禁止删除属性
const obj = {
name: '张三'
};
Object.defineProperty(obj, 'name', {
configurable: false // 禁止删除或重新定义
});
delete obj.name; // 删除无效
console.log(obj.name); // 张三
不足
1. 无法监听数组的变化
Object.defineProperty 除了直接对被监听的属性进行修改,无法直接监听数组的索引变化或长度变化。例如,调用某些数组方法(如 push、pop 等),它不会触发拦截逻辑。
const arr = [1];
let originalValue = arr[0]; // 保存原始值
Object.defineProperty(arr, '0', {
get() {
console.log('长度1',arr.length);
return originalValue; // 返回保存的值
},
set(value) {
console.log(`长度2`,arr.length);
originalValue = value; // 更新保存的值
}
});
arr.pop()
arr.push(4)
arr[0]=5// 长度1 1
arr[1]=6
可见pop和push对索引0的位置操作,并没有被监听到,为什么呢?
因为数组方法(如 push、pop 等)直接操作数组的底层存储结构,绕过了属性访问器。
就如同push方法相当于arr[arr.length]=5,这种写法本质上是一个动态赋值操作,js引擎会先计算出arr.length的值,然后将其作为索引直接写入数组底层结构
但若是使用arr[0]=5,这时索引已经被明确指定,js引擎就会检查索引是否被拦截,从而触发set或get
在 Vue.js 2.x 中,为了绕过这个限制,Vue 对数组的常用方法(如
push、pop等)进行了重写,但这增加了实现复杂度。
2. 性能问题
面对需要对多个属性进行监听的时候,需要给属性逐个定义Object.defineProperty,这会导致性能消耗增大。尤其是需要动态添加或者删除属性时,会频繁调用
// 使用 Object.defineProperty 定义属性
console.time('Object.defineProperty');
const obj1 = {};
for (let i = 0; i < 10000; i++) {
Object.defineProperty(obj1, `key${i}`, {
value: i,
writable: true,
enumerable: true,
configurable: true
});
}
console.timeEnd('Object.defineProperty');
// Object.defineProperty: 10.236ms
// 使用普通赋值
console.time('普通赋值');
const obj2 = {};
for (let i = 0; i < 10000; i++) {
obj2[`key${i}`] = i;
}
console.timeEnd('普通赋值');
// 普通赋值: 2.938ms
3. 无法拦截新增属性
Object.defineProperty 只能对已存在的属性进行定义或修改,无法自动拦截新增的属性。如果需要监听新增属性,必须手动调用 Object.defineProperty
const obj = {};
Object.defineProperty(obj, 'name', {
value: '张三',
writable: true,
enumerable: true,
configurable: true
});
obj.age = 25; // 新增属性不会触发拦截逻辑
console.log(obj.age); // 25
4. value和get同时使用会报错
const obj = {};
Object.defineProperty(obj, 'name', {
value: '张三',
get() { return '李斯'; } // 抛出错误
});
5. 不支持深度监听
const obj = {
info: {
name: '张三'
}
};
let originalValue = obj.info; // 保存原始值
Object.defineProperty(obj, 'info', {
get() {
console.log('获取 info');
return originalValue; // 返回保存的值
},
set(value) {
console.log('info 被修改');
originalValue = value; // 更新保存的值
}
});
// 修改顶层属性
obj.info = { name: '李四' }; // 输出: info 被修改
console.log(obj.info); // 输出: 获取 info { name: '李四' }
// 修改嵌套属性
obj.info.name = '二狗'; // 不会触发拦截逻辑
console.log(obj.info); // 输出: 获取 info { name: '二狗' }
总结
Object.defineProperty 的缺点主要集中在以下几个方面:
- 无法监听数组变化。
- 性能较低,尤其是在处理大量属性时。
- 无法拦截新增属性或删除属性。
- 不支持深度监听。
- 功能有限,无法拦截函数调用或其他操作。
所以在ES6出现了Proxy,而Object.defineProperty也逐渐被取代