深入对象

122 阅读6分钟

深入对象

对象是一组属性的无序集合

属性的类型

对象中会使用一些内部特性描述属性的特征,开发者不能直接访问这些属性,这些属性通常都有一个特征:用两个中括号把特性的名称括起来

属性分为两种:数据属性和访问器属性

数据属性

数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置

其中,数据属性有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:falseconfigurable: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方法