面试官:你了解属性描述符吗?
我:额(⊙o⊙)… , 恨不得把自己的脑子,打开看看怎么这么空白啊。
面试官:你平常没怎么学习过框架的源码吧,建议可以多看看。
心里想着,凉了下一家吧,他人还怪好的咧,又遇到一个新知识盲区。
什么是属性描述符?
前端宝宝解释:字面意思很好理解,描述一个属性的符号。
正规军解释 : 属性描述符是用于描述对象的属性特性以及其行为的一种机制。属性描述符包括数据描述符
和存取描述符
两种。
-
数据描述符: 数据描述符用于描述普通的数据属性,并包含以下相关属性:
- configurable:是否可配置。如果定义为true,则该属性可以被删除,且特性如下面的enumerable、writable和value能够被修改。如果设置为false,则该属性不可被删除,也不能被修改为访问器属性。(若为false则,不可通过Object.defineProperty()二次配置属性描述符)
- enumerable:是否可枚举遍历。如果设置为true,则该属性会出现在对象的for…in循环和Object.keys()函数的结果中。如果设置为false,则不会出现在这些结果中。
- writable:是否可写。如果设置为true,则该属性的值可以被修改;如果设置为false,则该属性的值不能被修改,就是只读属性。
- value:该属性的值。
-
存取描述符: 存取描述符用于描述getter/setter属性,并包含以下相关属性:
- configurable:是否可配置(同上)。
- enumerable:是否可枚举(同上)。
- get:获取函数或方法。
- set:设置函数或方法。
如何获取属性的属性描述符呢?
可通过 Object.getOwnPropertyDescriptor()
let obj = {
a: 10,
c: 20,
}
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// configurable: true
// enumerable: true
// value: 10
// writable: true
想了解更详细的信息,详见MDN。
如何配置属性描述符呢?
可通过 Object.defineProperty()
相信大多数小伙伴和我一样,看到这个方法,顿时虎躯一震。话不多说直接上代码先感受一下,它的魅力。
var obj = {};
Object.defineProperty(obj, 'myProperty', {
configurable: true,
enumerable: true,
writable: false,
value: 'example'
});
console.log(obj.myProperty); // 输出 "example"
obj.myProperty = 'new value'; // 尝试修改值
console.log(obj.myProperty); // 仍然输出 "example"
通过Object.defineProperty
配置完毕,当我想重写这个属性的值时,它已经变成一个只读属性了。 当然这只是我们主角能力的冰山一角,还有我们较为熟悉的set() 和 get() 方法还没出场呢,这两大哥就不多介绍了大家都是老熟人了。
- get() : 读取器
- set() : 设置器 (合称:访问器)
直接上代码
class myProperty {
constructor(g) {
// 定义属性
Object.defineProperty(this, 'data', {
get: function () {
console.log('data被读取了');
return g
},
set: function () {
throw new Error('data属性不可重新赋值')
},
configurable: false, // 是否可再次配置属性描述符
})
}
}
let my = new myProperty(obj)
let you = my.data // data被读取了
my.data = 'abc' // Uncaught Error: data属性不可重新赋值 at myProperty.set
在vue中get函数那步就是我们熟悉的,依赖收集,set函数就是 派发更新。
上述代码看起来非常简单是吧,但其实有两个细节是需要注意的:
- 在第18行中,左边的 my.data 也会触发get方法,data被读取了其实会打印两个,然后再去执行set方法,直接抛出错误。
- 不可以同时指定getter、setter访问器和值(value)或可写属性(writable)。否则会报错:
Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> at Function.defineProperty (<anonymous>) at new myProperty
上述代码就简单实现了,一个属性的只读功能,并会提示报错,避免写到后面了发现值没有赋上,不知道问题在哪里。当然也还是有缺陷的,只能是最表面的一层,再下一层就控制不了,那就又要用到一个新方法了。见如下扩展…
扩展1:
Object.freeze()
作用: 可以使一个对象被冻结。冻结对象可以防止扩展,并使现有的属性不可写入和不可配置。被冻结的对象不能再被更改:不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定。freeze()
返回与传入的对象相同的对象。详见MDN。 有了这个方法我们就可以这样了。
class myProperty {
constructor(g) {
let myG = {...g} // 克隆一份源数据,保证不影响数据源
Object.freeze(myG) // 将数据冻结
// 定义属性
Object.defineProperty(this, 'data', {
get: function () {
console.log('data被读取了');
return myG
},
set: function () {
throw new Error('data属性不可重新赋值')
},
configurable: false, // 是否可再次配置属性描述符
})
}
}
let my = new myProperty(obj)
let you = my.data // data被读取了
扩展2
除了Object.freeze()
方法,JavaScript还提供了一些与其类似的方法,可以用于改变对象的可变性和保护对象的数据完整性:
Object.seal(obj)
:在原有对象上封闭(seal)该对象,并返回该对象。和freeze
相比,封闭后的对象仍然可以修改属性值,但不能添加新属性,也不能删除已有属性(但是可以更改属性值)。使用这个方法可以保护对象的属性不被意外地删除。详见MDN。Object.preventExtensions(obj)
:阻止向对象添加新属性。可以理解为冻结对象的“添加属性”功能,而不像freeze
防止该对象属性值发生变化或被删除。详见MDN。
需要注意的是,这些方法都只能保护对象属性的基本数据类型,对于引用类型的属性并不能完全保护。
const obj = {
a: [1, 2, 3]
};
Object.seal(obj);
obj.a.push(4); // 可以修改数组引用类型属性
console.log(obj.a); // 输出 [1, 2, 3, 4]
Object.preventExtensions(obj);
obj.b = 'new property'; // 不可额外添加属性
console.log(obj.b); // 输出 undefined
在上述示例中,通过Object.seal
方法可以封闭obj
对象并禁止额外添加新属性,但是因为a
属性是一个数组,其中包含的元素并没有被冻结,所以push()
方法可以用于修改数组值并且不会报错。如果要保护a
属性中的元素,则需要使用递归方法
冻结每个对象。
总之,这些方法都是用于JavaScript对象保护和数据完整性的工具,具有不同程度的限制和保护能力,开发者可以根据实际需求选择适合的保护方式。
当我学习这里的时候,突然感觉脑子callback
了一下,理解面试官建议我去学习一下,框架或者第三方库的源码,想必大家也明白了吧,哈哈哈…
面试真是的一个查漏补缺的好方法,从面试官的一个问题,结束后的深入学习,收获知识与分享知识,真的是一个非常美妙的过程,丝毫没有觉得累和心里的不舒服。遇到一个优秀的面试官,可能真的会对自己帮助挺多的,加油,努力向一名优秀的面试官前进!