一、属性描述
属性描述用来描述一个属性的行为,具体的属性描述符如下:
- value,属性的值
- writable, 属性是否可写
- enumerable,属性是否可被遍历,如for in
- configurable,属性是否可以被重新描述
- get 属性访问器, getter, 读取属性时会调用这个函数获取它的返回值作为结果。
- set 属性访问器, setter, 当给属性赋值时会调用这个函数, 并将赋值作为参数传入。
语法:
js
//属性描述
Object.defineProperty(obj, 'name', {
value: 'jack',
writable: true,
});
// 获取obj.name的属性的描述
Object.getOwnPropertyDescriptor(obj, 'name');
// 获取obj下所以字段的属性描述
Object.getOwnPropertyDescriptors(obj);
1.1 writable
user对象使用属性描述新增了一个gender属性,writable描述为false,该属性无法被修改
js
const user = {
name: 'jack',
age: 21
}
Object.defineProperty(user, 'gender', {
writable: false,
value: '男',
});
user.gender = '女';
console.log(user.gender); //男
1.2 enumerable
enumerable设置为false之后该属性不能再被遍历到
js
Object.defineProperty(user, 'gender', {
writable: false,
value: '男',
enumerable: false,
});
for(key in user){
console.log(key);
}
// name, age
1.3 configurable
上面的例子中gender属性被描述为不可写,但是仍然可以通过重新定义属性描述破坏这个"规则"。
js
const user = {
name: 'jack',
age: 21
}
Object.defineProperty(user, 'gender', {
writable: false,
value: '男',
enumerable: false,
configurable: true,
});
Object.defineProperty(user, 'gender', {
writable: true,
value: '女',
enumerable: true,
configurable: true,
});
上面的代码修改了gender并重新设置了可写, 为了使gender真正的不能被修改,需要把configurable也设置为false。
需要注意的是在使用defineProperty的时候如果没有手动声明configurable则该描述符就默认为false。理解起来就是属性不能被reDefineProperty,除非手动声明了configurable: true。
js
Object.defineProperty(obj, 'name', {
value: 'jack',
});
// 等同于上面的代码
Object.defineProperty(obj, 'name', {
value: 'jack',
writable: false,
});
1.4 get和set
get和set统称为访问器。
js
const user = {
name: 'jack',
}
templeAge = 0;
Object.defineProperty(user, 'age', {
set(_age) {
if (typeof _age !== 'number') {
throw new Error(`age必须为数字类型, 当前age的类型为: ${typeof _age}`);
} else if (_age < 0) {
throw new Error(`age不能小于0, 当前age的值为: ${_age}`);
} else if (!Number.isInteger(_age)) {
throw new Error(`age必须是整数, 当前age的值为: ${_age}`);
}else {
templeAge = _age;
}
},
get() {
return `age is: ${templeAge}!`;
}
});
user.age = '10'; // Uncaught Error: age必须为数字类型, 当前age的类型为: string
user.age = 1;
console.log(user.age); //age is: 1!
当使用访问器读取和设置属性的时候都会经过set、和get函数。因此可以在set函数中对设置的值进行拦截操作在,做一些数值、类型或者其它逻辑的判断,当不符合条件的时候抛出对应的自定义错误。在get函数中也可以进行一些拦截操作,比如属性计算,对原来的属性进行一些逻辑计算之后再进行返回。
访问器的死循环
思考下上面为什么单独使用一个templeAge临时变量,而不是直接像下面的代码直接使用user.age?
js
Object.defineProperty(user, 'age', {
set(_age) {
user.age = _age;
},
get() {
return user.age;
}
});
因为user.age本身是一个读取操作,会执行get获取到返回值,而get内部也有user.age的逻辑也会执行get函数,就陷入了死循环。set同理。
二、购物车案例
js
class ShoppingCard {
tempChoose = 0
constructor(data) {
Object.defineProperty(this, 'data', {
writable: false,
value: data,
});
Object.defineProperty(this, 'choose', {
set(_val) {
if (Number.isInteger(_val) && _val >= 0) {
this.tempChoose = _val;
} else {
throw new Error('choole必须为大于0的正整数')
}
},
get() {
return this.tempChoose;
}
})
}
// calss中属性修饰的语法糖
get totalPrice() {
return this.data.price * this.tempChoose;
}
}
const shoppingCard = new ShoppingCard({
price: 10,
id: '15896832',
classify: '日常百货',
name: '粉色牙刷'
});
三、总结
属性描述可以控制对象属性的行为,增加代码的稳健性,get、和set提供了劫持读写操作的能力,具有很大的操作空间。 vue2的依赖监听就是通过属性描述的访问器实现的。
四、拓展
属性描述符只能用来修饰对象的某个属性,如果这个被修饰的属性也是对象呢?
js
const user = {
name: '小赵',
}
Object.defineProperty(user, 'friends', {
writable: false,
value: {
a: '小赵',
}
});
user.friends.a = '小张';
user.friends.b = '小王';
console.log(user.friends); // {a: '小张', b: '小王'}
可以发现虽然friends不能被修改,但是friends下面的属性仍然可以新增和修改,这个时候就需要Object.freeze(user.friends)把对象进行冻结了,被冻结的对象不能进进行读以外的任何操作。此外还可以使用Object.seal(friends)将对象进行封存,被封存的对象只能修改属性的值,不能新增和删除属性。 需要注意的是freeze和seal操作都是不可逆的,如果不想影响到原本的对象,需要拷贝一下原数据再使用。