深入对象
对象是一组属性的无序集合
属性的类型
对象中会使用一些内部特性描述属性的特征,开发者不能直接访问这些属性,这些属性通常都有一个特征:用两个中括号把特性的名称括起来
属性分为两种:数据属性和访问器属性
数据属性
数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置
其中,数据属性有4个特性(可以用Object.defineProperty()设置):
-
[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改他的特性,以及是否可以把他改为访问器属性默认情况下,
[[Configurable]]的默认值为true例外:在该属性为
false的时候,还是可以把writable的状态从true改为false,但是不能从false改为true -
[[Enumberable]]:表示属性是否可以通过for...in循环返回默认情况下,
[[Enumberable]]的默认值为true -
[[Writable]]:表示属性的值是否可以修改默认情况下,
[[Writable]]的默认值为true -
[[Value]]:包含属性实际的值默认情况下,
[[Value]]默认值为undefined
上述所有默认值都是对于直接定义在对象上的属性才适用的
如果现在使用Object.defineProperty()这个API调用,那么没有传递的值就全部会默认为false
访问器属性
访问器属性不包含数据值
同样,访问器属性也有4个特性(可以用Object.defineProperty()设置):
-
[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改他的特性,以及是否可以把他改为数据属性默认情况下,
[[Configurable]]的默认值为true -
[[Enumberable]]:表示属性是否可以通过for...in循环返回默认情况下,
[[Enumberable]]的默认值为true -
[[Get]]:获取函数,在读取属性时调用,默认值为undefined -
[[Set]]:设置函数,再写入属性时调用,默认值为undefined
只定义获取函数意味着属性是只读的,尝试修改属性会被忽略,类似的,只有一个设置函数的属性是不能读取的
定义多个属性
之前我们如果要使用defineProperty方法去定义属性的特性的话,我们必须一个一个属性定义,不能一次性定义多个属性
所以现在有一个新的方法**Object.defineProperties,可以通过多个描述符一次性定义多个属性**
接收两个参数:要为之添加或修改属性的对象和另一个描述符对象
读取属性的特性
前面我们定义了属性的特性,所以现在我们可以通过一个方法来取得指定属性的属性描述符:Object.getPropertyDescriptor()
这个方法可以接收两个参数:属性所在的对象和要取得其描述符的属性名
返回值是一个对象,包含该属性的各个属性描述符
ES2017引入了**Object.getOwnPropertyDescriptors()** 这个方法,会返回所有属性的属性描述符
合并对象
JS中,合并两个对象是很有必要的,所以ES6为我们提供了一个方法可以让我们合并对象:Object.assign()
这个方法接收一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举和自有的属性复制到目标对象,以字符串和符号为键的属性会被复制
对符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性值,然后使用目标对象上的[[Set]]设置属性值
并且,还有一些细节:
Object.assign实际上对每个源对象执行的是浅复制- 如果多个源对象都有相同的属性,就用最后一个复制的值
Object.assign没有回滚之前赋值的概念,如果赋值期间出现错误,那么可能只会完成部分复制
对象标识及相等判定
ES5比较两个值是否相等,只有两种运算符:==和===
==:会自动转换数据类型===:NaN不等于自身,以及+0等于-0
ES6新增了一个方法**Object.is()** ,这个方法和===很像,但是用来比较两个值是否严格相等,并且能解决上述边界值
console.log(Object.is(true, 1)) //false
console.log(Object.is({}, {})) //false
console.log(Object.is("2", 2)) //false
console.log(Object.is(+0, -0)) //false
console.log(Object.is(+0, 0)) //true
console.log(Object.is(-0, 0)) //false
console.log(Object.is(NaN, NaN)) //true
如果要检查超过两个值,则使用递归即可:
function recursivelyCheckEqual(x, ...rest){
return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest))
}
不变性
如果我们需要一个对象是不可改变的,我们就需要重新思考一下怎么实现
因为现在的所有方法都只能实现浅不变性,也就是说,他们只会影响目标对象和直接属性,如果目标对象引用了其他对象,那么其他对象的内容不受影响,仍然是可变的
obj.foo //[1, 2, 3]
obj.foo.push(4)
obj.foo //[1, 2, 3, 4]
所以现在,我们可以考虑一下几个角度综合起来去实现对象的不变性:
-
对象常量
结合
wrtitable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除)let obj = {} Object.defineProperty(obj, 'FAVORITE_NUMBER', { value: 42, writable: false, configurable: false }) -
禁止扩展
禁止一个对象添加新属性并且保留已有属性,可以用
Object.preventExtensions()非严格模式下,添加属性会静默失败,严格模式下会抛出异常
let obj = { a: 2 } Object.preventExtensions(obj) obj.b = 3 console.log(obj.b) //undefined -
密封
我们将上述两个方法综合起来,也就是让一个对象既不能添加属性,也让所有现有属性不能配置和删除
Object.seal()就能做到这一点,能够创建一个密封的对象,符合上述要求但是,这个方法只是让属性不能修改配置,并没有限制对属性值的修改
let obj = { a: 2 } Object.seal(obj) obj.b = 3 delete obj.a console.log(obj.a) // 2 console.log(obj.b) // undefined -
冻结
上面我们讲过,密封的缺点就是能够修改属性值
所以**
Object.freeze()** 就提供了这个功能,它不仅能调用Object.seal(),还能把所有数据访问的属性标记为writable:false,这样就无法修改它们的值但是这个方法还是不能限制这个对象引用的其他对象
let obj = { a: 2 } Object.freeze(obj) obj.b = 3 obj.a = 4 console.log(obj.a) // 2 console.log(obj.b) // undefined由于不能限制对象内部引用的对象,所以我们可以采取深度冻结的方法,遍历他引用的所有对象并在这些对象上调用
Object.freeze方法