ES中对象的扩展

Posted Tiger老师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES中对象的扩展相关的知识,希望对你有一定的参考价值。

属性的可枚举性

可枚举性

对象的每个属性都有一个描述对象(Desciprtor),用来控制该属性的行为。Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象

    let obj = {foo: \'foo\'}
    Object.getOwnPropertyDescriptor(obj, \'foo\')
    /*
        {
            value: \'foo\',
            writable: true,
            enumerable: true,
            configurable: true
        }
    */ 

描述对象的enumerable属性,称为’可枚举性’, 如果该属性为false,表示某些操作会忽略当前属性

目前,有四个操作会忽略enumerable为false的属性

  • for in 只遍历对象自身和继承的可枚举属性
  • object.keys 只返回可遍历的属性名
  • Json.stringify 只串行化对象自身的可枚举的属性
  • Object.assign 忽略enumerable为false的属性,只拷贝对象自身的可枚举属性

引入可枚举性这个概念的最初目的,就是让某些属性可以避免被for…in,否则内部的方法,属性都会被遍历到。比如对象的tostring,数组的length

    Object.getOwnPropertyDescriptor(Object.prototype, \'toString\').enumable
    // false

    Object.getOwnPropertyDescriptor([], \'length\').enumable
    // false

上面代码对象的toString 和数组的length 属性的enumable 都是false,所以不会被for…in遍历到

ES6规定所有class的继承属性都是不可枚举的

    Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, \'foo\').enumerable
    // false

由于for…in总是引入继承的属性,所以尽量使用Object.keys

属性的遍历

ES6一共有五种方法,可以遍历对象的属性

(1)for…in
for…in遍历对象自身和继承的可枚举的属性(不包含Symbol)

(2)Object.keys(obj)
Object.keys返回一个数组,包含自身所有(不含继承)可枚举的属性(不含Symbol)键名

(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身所有属性(不含Symbol,含有不可枚举)属性的键名

(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols 返回一个数组,包含自身所有包含Symbol属性的键名

(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身所有属性(不含继承),不管键名是否是Symbol,不管是否可枚举

以上的5种方法遍历对象的键名,都遵循同样的属性遍历的次序规则

首先遍历所有数值键,按照数值升序排列
其次遍历所有字符串键,按照加入时间升序排列
最后遍历所有Symbol键,按照加入时间升序排列

    let obj = {
        [Symbol()]: 0,
        b: 0,
        3: 9,
        10: 20,
        a: 9
    }
    Reflect.ownKeys(obj)
    // [3, 10, b, a, Symbol()]

Reflect.ownKeys方法会返回一个数组,这个数组的返回次序是这样的,

首先是数值3和10,其次是字符串b和a,最后是symbol

super关键字

this总是指向函数所在的当前对象,ES6新增了super关键字,它指代的是当前对象的原型对象

this --> 当前对象
super --> 当前对象的原型对象

    let person = {
        name: \'person\'
    }

    let son = {
        name: \'sun\',
        printPerson () {
            return super.name
        }
    }
    
    Object.setPrototypeOf(son, person)
    son.printPerson()
    // \'person\'

上面通过设置son的原型,调用son方法来找到原型的属性

super表示原型对象时,只能用在对象的方法之中,用在其他地方会报错

    let obj = {
        foo: super.foo
    }
    // 用在了属性上

    let obj1 = {
        foo: () => super.foo
    }
    // 用在了方法

    let obj2 = {
        foo: function () {
            return super.foo
        }
    }

上面三种都会报错,因为对于js引擎而言,根本没有用到super。

第一种属于用在属性中
第二种和第三种属于用在函数中,但又赋值给了foo属性

现在只有简写的对象方法才能让js引擎确认,定义的是对象的方法

js引擎内部,super.foo等同于
Object.getPrototypeOf(this).foo 属性 或 Object.getPrototypeOf(this).foo.call(this) 方法。

    let father = {
        name: \'person\',
        sayHello () {
            return this.name
        }
    }

    let son = {
        name: \'son\',
        sayHi () {
            return super.sayHello();
        }
    }
    
    Object.setPrototypeOf(son, father);
    son.sayHi();
    // \'son\'

上面代码分为三步

  • 第一步调用son.sayHi 函数返回了原型的sayHello方法
  • 第二步father自身调用sayHello方法,返回了this.name
  • 第三部this.name 此时的this,因为在son的环境执行的所以,this指向son,所以打印结果为’son’

如果改为father.sayHello() 就不一样了

对象的扩展运算符

数组中的扩展运算符(…)以及得心应手了,对象的写法与之功能基本类似

解构赋值

对象解构用于将一个对象的值全部取出来(可遍历的),并且没有被读取的属性,赋值到另一个对象身上,所有他们的键和值,最后形成一个新对象

    let {x, y, ...z} = {x: 1, y: 2, z: 3, u: 10, n: 20}
    x // 1
    y // 2
    z // {z: 3, u: 10, n: 20}

上面代码只有z是解构成功的对象,x和y属于被读取过后的值了,z会把x y没有读取到的键和值拷贝过来,返回一个新数组

解构赋值等号右边必须是一个对象,如果不是就会报错

    let {...y} = undefined // error
    let {...n} = null        // error 

解构赋值必须是最后一个参数,否则会报错

    let {...x, y} = obj; // error
    let {x, ...y, z} = obj; // error

解构赋值是浅拷贝,即如果拷贝的值是一个数组,对象,函数,那么拷贝的实际是引用,而不是副本

    let obj = { a: {b: 1} }
    let {...newObj} = obj
    newObj.a.b = 2;
    obj.a.b
    // 2

上面代码拷贝的是一个对象,由于解构只是浅拷贝,所以指向了同一个指针地址,导致新地址的数据改变,原地址指向的数据也要发生改变,因为他们的指向同一个房间。

解构赋值无法拿到原型的属性

    let a1 = {bar: \'bar\'}
    let a2 = {foo: \'foo\'}
    Object.setPrototypeOf(a1, a2)
    let {...a3} = a2
    a3
    // {foo: \'foo\'}
    a3.bar
    // undefined

上面代码a2 继承a1,a3又拷贝了a2 ,但是解构赋值无法拿到原型属性,导致a3没有获取到a1这个祖先的属性

    // 创建一个obj的原型对象
    let obj = Object.create({x: 1, y: 2})
    // 自身添加一个z属性
    obj.z = 3
    
    let {x, ...newObj} = obj
    // x是单纯的解构赋值,所以可以读取继承属性。
    // y和z是扩展运算符解构赋值,只能读取对象自身的属性
    let {y, z} = newObj
    // y是继承属性,所以newObj中获取不到。
    x // 1
    y // undefined
    z // 3

可能有人觉得可以省事,会这么写

    let {x, ...{y, z}} = obj
    // error

ES6规定,变量声明语句中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式。

解构赋值的一个用处,是扩展某个函数的参数,传递给另一个函数或者引入其他操作

    function test (a, b) {
        return a + b
    }
    
    function bar (x, y, ...anther) {
        // 使用x y 进行内部操作
        // 把扩展运算符导出出去,提供给另一个函数,或者做别的操作
        console.log(x * y);
        return test(anther);
        
    }

    bar(1, 3, 9, 9)

扩展运算符

对象的扩展运算符,用于取出参数对象所有可遍历属性,拷贝到当前对象之中

    let z = {x: 1, y: 2}
    let n = {...z}
    n // {x: 1, y: 2}

由于数组是特殊的对象,所以对象扩展运算符也可以用于数组

    let foo = {...[\'x\', \'y\', \'z\']}
    foo 
    // {0: \'x\', 1: \'y\', 2: \'z\'}

如果扩展运算符后面是一个空对象,则没有任何效果

    let obj = {...{}, a: 1}
    obj
    // {a: 1}

如果不是个对象,会把他转换为对象

    // {...Object(1)} 等同于
    {...1}
    // {}

上面会调用包装类,转换为Number(1) 但该对象自身本就没有任何属性,所以返回空

还有一些类型

    {...true} // {}
    {...undefined} // {}
    {...null} // {}
    {...\'es6\'} // {0: \'e\', 1: \'s\', 2: \'6\'}

前三个都会返回空对象,最后一个字符串,由于字符串会转换为类数组,所以返回的就是一组带索引的对象

对象的扩展运算符等同于使用Object.assign() 方法

    let aClone = {...a};

    // 等同于
    let aClone = Object.assign({}, a)

上面只是拷贝对象实例属性,想要拷贝原型属性,需要这样写

    // fn1 
    let clone1 = {
        // 拿到obj的原型,并赋给自己的proto, 自己的原型指向了obj的原型 
        __proto__ : Object.getPrototypeOf(obj),
        // obj的实例属性赋给自己原型,拷贝obj的实例属性和原型属性
        ...obj
    }

    // fn2
    let clone2 = Object.assign(
        // 创建一个对象,该对象的原型是obj原型对象
        Object.create(Object.getPrototypeOf(obj)),
        // 把obj放到上面这个对象里
        obj
    )
    
    // fn3
    let clone3 = Object.create(
        // 创建参照对象,该对象为obj的原型对象
        Object.getPrototypeOf(obj),
        // 把obj的所有可枚举属性传递给参照对象
        Object.getOwnPropertyDescriptors(obj)
    )

扩展运算符,可以合并对象

    let obj1 = {...a, ...b}
    let obj2 = Object.assign({}, a, b)

如果扩展运算符后面还有用户自定义的属性,那么扩展运算符内部的同名属性会被覆盖掉

    let obj = {...a, x: 1, y: 2}
    // 等同于
    let obj1 = {...a, ...{x: 1, y: 2}}
    // 等同于
    let x = 1, y = 2, obj2 = {...a, x, y}
    // 等同于
    let obj3 = Object.assign({}, a, {x: 1, y: 2})

上面代码中,a对象中的x, y属性都会被 a后面的 x y都会在拷贝到新对象时覆盖掉

这样一来更改部分现有属性,很方便

    let newVersion = {
        ...oldVersion,
        name: \'new Version\'
    }

上面的旧版本 name会被替换掉

如果自定义属性放在扩展运算符前,就会变成设置新对象的默认属性值

    let obj = {x: 1, y: 2, ...a}
    ==
    let obj1 = Object.assign({}, {x: 1, y: 2}, a
    == 
    let obj2 = Object.assign({x: 1, y: 2}, a)

对象扩展运算符后面也可以跟表达式

    let obj = {
        ...(x > 1 : {name: \'wei\'} ? {}),
        bar: \'foo\'
    }

如果扩展运算符的参数对象之中,有取值函数get,它会自动执行的

    let obj = {
        get foo () {
            console.log(1)
        }
    }
    
    let newObj = {...obj}
    // 1

上面代码,在扩展结束后就会执行get函数的语句

以上是关于ES中对象的扩展的主要内容,如果未能解决你的问题,请参考以下文章

ES 6 : 数组的扩展

Babel 无法使用扩展运算符编译 ES6 对象克隆

ES6 对象的扩展

ES6解构赋值

ES6对象扩展

ES6正则扩展