关于JavaScript对象,你所不知道的事(二)- 再说属性
说完了对象那些不常用的冷知识,是时候来看看JavaScript中对象属性有哪些有意思的东西了。
不出你所料,对象属性自然也有其相应的特征属性,但是这个话题有点复杂,让我们先从简单的说起,对象属性的分类。
面对一个复杂的事物,寻找其内在共性,妥善分类往往是快速认知该事物的捷径,这与程序员“将难以解决的大问题拆解为可以解决的小问题”的思维有异曲同工之妙。
那么,对象的属性根据不同的维度,可以如何分类呢?你或许想不到,竟然有如此多的分类方法,而不同的类别,有牵扯出特定的方法解决这一类别的某些问题。让我们看看吧:
按来源分类
- 私有属性
- 原型属性
JavaScript是一门基于原型链的语言,对象继承是节省内存空间,避免代码重复,逻辑混乱的好方法。而对象继承对于属性而言则带来一个问题,即我们需要区分某对象内的属性,究竟是对象自有的(私有属性),还是继承于其他对象(继承属性),无论是进行属性遍历还是对属性进行操作,我们都需要谨慎的思考这个问题。
让我们举例两个典型场景看看JavaScript是如何帮助我们解决这个问题的:
情景一: 属性查找
有时候,我们需要查找某个对象是否有某个属性,再进一步决定是否要执行下一步操作,JavaScript提供给我们的查找工具是in
操作符,in
操作符用以在给定对象中查找一个给定名称的属性,如果找到则返回true值。实际上,in
操作符就是在哈希表中查找一个键是否存在(还记的我们的蓝色章鱼吗,in
操作符只是检查章鱼是否有那只触角,并不关心触角上拿着的卡片上写了什么,更不会随着卡片的地址去读取值,这对于提升性能尤其重要),让我们看看代码示例:
let obj = {
x: 1,
}
console.log('x' in obj) // true
console.log('y' in obj) // false
但是遗憾的是,in
操作符会检查所有的私有属性和原型属性,因此你并不能通过in
操作符知道该属性的真正来源。
但好在JavaScript还给我们提供了一个.hasOwnProperty()
方法,每一个对象都有这样一个方法,专门用来判断某个属性是否是该对象的私有属性。
我们终于得到了我们想要的。太棒了。
小结:
- 查找属性(不区分属性来源):
in
操作符 - 查找私有属性:对象的
.hasOwnProperty()
方法
情景二: 属性枚举
有些时候,我们想要获得一个对象内所有属性的键或值(或者全部都要),这时我们就要枚举一个对象内的所有属性,通常,我们会使用for-in
循环去实现这一点。
然而,很不巧的是,for-in
循环会遍历所有可枚举的原型属性,注意这里有两点需要进一步说明:
- 可枚举:这牵扯到我们很快要谈到的属性特征属性(有点拗口是吧:))
- 会遍历原型属性:这样当一个对象的继承链很长而我们又只关心对象的私有属性时就会变得非常麻烦
当然,你可以在for-in
循环中,再使用我们刚提到的.hasOwnProperty()
方法,但是JavaScript给予了我们更好的选择:使用Object.keys()
方法:
Object.keys()
方法是ECMAScript5引入的方法,它可以获取可枚举属性的名字的数组,并且它只返回对象的自有属性。
因此,你可以基于是否需要一个数组,是否只需要对象自有属性来判断使用哪一种方法。
小结:
- 枚举属性(不区分属性来源):
for-in
循环 - 只枚举私有属性,且返回数组:
Object.keys()
方法
按作用分类
- 数据属性
- 访问器属性
你也许很少听说过这样的分类方式,因为我们几乎都在使用数据属性,让我来简要说明这两种类型的属性的区别:
数据属性包含了一个值,我们之前提到的对象的内部方法[[Put]]的默认行为就是创建数据属性:
let obj = {
x: 1, // x 是数据属性
}
访问器属性不包含值,而是定义了一个当属性被读取时调用的函数(称为“getter”)和一个当属性被写入时调用的函数(称为“setter”):
let obj = {
x: 1,
get y() {
return 2
}
set y() {
return 3
}
}
console.log(obj.y) // 2
之所以访问器属性很少见到是因为我们很少需要在进行属性赋值或读取操作时触发一些行为,不过反过来说,如果这恰恰是你面临的场景,就大胆的使用吧。
对象属性的特征属性
绕了一大圈,终于可以回到正题,谈谈属性的特征属性了,相较于对象只有一个孤零零的[[Extensiable]]
特征属性,对象属性要复杂的多:
因为所有对象属性都具有:
[[Enumerable]]
特征属性:决定一个属性是否可以被遍历;[[Configurable]]
特征属性:决定一个属性是否可以被配置
而只有数据属性有以下两个属性:
[[Value]]
特征属性:即属性的值;[[Writable]]
特征属性:值为布朗类型,决定该属性值是否可以写入;
而只有访问器属性有以下两个属性:
[[Get]]
特征属性:即为getter函数内容;[[Set]]
特征属性:即为setter函数内容;
让我们先来看看这些特征属性的意义,再来谈谈如何配置这些特征属性:
[[Enumerable]]
并不是所有的属性都是可枚举的,实际上,对象的大部分原生方法的[[Enumerable]]特征属性的值都被设置为false(所以使用for-in
循环时,不会遍历出一大堆你不需要的内容),那我们该如何判断一个属性是否是可枚举的呢?
JavaScript为我们提供了.propertyIsEnumerable()
方法去检查一个属性是否可枚举,像.hasOwnProperty()
方法一样,每个对象都拥有这个方法:
let obj = {
x: 1,
}
console.log(obj.propertyIsEnumerable('x')) // true
[[Configurable]]
可配置是指:
- 删除操作;
- 属性类型变更操作(从数据属性变为访问器属性,或者相反):
- 使用
Object.defineProperty()
方法配置属性(别着急,我们之后会着重讲解这个方法);
因此,当你设置某个属性的[[Configurable]]
特征属性为false
时,以上三种操作就都不能正确执行。
配置特征属性
是时候讲解JavaScript为我们提供的配置属性特征属性的方法了:Object.defineProperty()
该方法接收三个参数:
- 拥有该属性的对象
- 属性名(字符串)
- 包含需要设置的特征的属性描述对象(属性描述对象具有和特征属性额同名的属性,但是名字中不包含中括号)
让我们看看该方法的实际用法:
let obj = {
x: 1,
}
Object.defineProperty(obj, 'x', {
enumerable: false,
})
console.log('x' in obj) // true
console.log(obj.propertyIsEnumerable('x')) // false
我们通过Object.defineProperty()
方法使obj对象的x属性为不可遍历的,在之后的检测中,我们看到控制台输入属性存在,但不可遍历。
让我们再看看数据属性配置特征属性的示例:
let obj = {
x: 1,
}
Object.defineProperty(obj, 'x', {
value: 2,
enumerable: true,
configurable: true,
writable: true,
})
console.log(obj.x) // 2
// 注意我们所做的实际上完全等同于以下这段代码
let obj = {
x: 2,
}
下面是访问器属性配置特征属性的示例:
let obj = {
x: 1,
}
Object.defineProperty(obj, 'x', {
get: function() {
console.log('reading...')
return this.x
},
set: function(value) {
console.log('setting...')
this.x = value
},
enumerable: true,
configurable: true,
})
使用访问器属性特征属性比使用对象字面形式定义访问器属性的优势在于,你可以为已有的对象定义这些属性。如果你想要用对象字面形式,你只能在创建对象时定义访问器属性。
需要注意的是,一旦你决定使用Object.defineProperty()方法配置属性的特征属性,你需要完整在配置对象中列出enumerable属性与configurable属性,因为在默认情况下,这些属性的值皆为false,这可能不是你想要的。
定义多重属性
当你需要配置一个对象的多个属性时,你需要使用Object.defineProperties()
方法,其用法如下:
let obj = {
x: 1,
}
Object.defineProperties(obj, {
x: {
value: 2,
enumerable: true,
configurable: true,
writable: true,
},
y: {
get: function() {
console.log('reading...')
return this.x
},
set: function(value) {
console.log('setting...')
this.x = value
},
},
})
获取属性特征属性
目前为止,我们提到了属性的所有特征属性,以及如何设置,最后,让我们看看JavaScript为我们提供的查看属性特征属性的方法:Object.getOwnPropertyDescriptor()
。其用法如下:
let obj = {
x: 1,
}
const descriptor = Object.getOwnPropertyDescriptor(obj, 'x')
console.log(descriptor.enumerable) // true
console.log(descriptor.configurable) //true
console.log(descriptor.writable) // true
console.log(descriptor.value) // 1
可以看到,该方法接收两个参数,一个目标对象以及想要获取特征属性的属性名,该函数会返回一个特征属性描述对象,包含属性特征属性的所有信息。
难以置信,我们终于讲完了属性的所有特征属性。看到这里的你也值得为自己鼓掌👏
先休息一会吧,然后我们看看最后的一个主题(还记的我们上一章提到的封闭对象吗?),定义禁止修改的对象。
对象封印与对象冻结
对象封印
对象封印是指,通过使用Object.seal()
方法使一个对象不仅不可扩展,其所有的属性都不可配置,也就是说,对于一个被封印的对象,你不能:
- 添加新属性;
- 删除属性或改变属性类型;
当一个对象被封印时,你只能读写它已有的属性。另外,我们可以通过Object.isSealed()
方法检验一个对象是否为被封印对象。
代码如下:
let obj = {
x: 1,
}
console.log(Object.isExtensible(obj)) // true
console.log(Object.isSealed(obj)) // false
// 封印对象
Object.seal(obj)
console.log(Object.isExtensible(obj)) // false
console.log(Object.isSealed(obj)) // true
obj.y = 2
console.log('y' in obj) // false
obj.x = 3
console.log(obj.x) // 3
delete obj.x
console.log(obj.x) // 3
对象冻结
让我们好好想想对象封印都做了些什么,它使我们不能添加属性,只能对已有的属性进行读写操作,但却无法改变已有属性的特征属性,也无法删除已有属性,我们的对象的封闭性已经非常强了。
而对象冻结则更近一步,将对象属性的操作限制为只读,它更像是一个对象某一时刻的快照,除了看之外我们不能对它有任何操作。
在JavaScript中,我们使用Object.freeze()
冻结一个对象,并且使用Object.isFrozen()
来判断一个对象是否被冻结。
终于结束了,让我们简短回顾一下我们在本章中都讲了些什么:
-
首先,我们讲到了属性的分类:
- 按来源分:私有属性 和 原型属性
- 按作用分:数据属性 和 访问器属性
-
其次,我们谈到了属性的特征属性:
- 共有特征属性:
[[Enumerable]]
,``[[Configurable]]` - 数据属性特征属性:
[[Value]]
,[[Writable]]
- 访问器属性特征属性:
[[Set]]
,[[Get]]
- 共有特征属性:
-
以及:
- 配置属性特征属性的方法:
Object.defineProperty()
- 定义多重属性特征属性的方法:
Object.defineProperties()
- 获取属性特征属性的方法:
Object.getOwnPropertyDescriptor()
- 配置属性特征属性的方法:
-
最后,我们介绍了定义更加封闭对象的两种方式:
- 对象封印:
Object.seal()
,Object.isSealed()
方法用于检验一个对象是否被封印 - 对象冻结:
Object.freeze()
,Object.isFrozen()
方法用来判断一个对象是否被冻结
- 对象封印:
大功告成!你现在已经和我一样完全了解JavaScript对象了,Good Job👏