愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之对象的扩展

一、属性的简洁表示法

当对象中属性的key名称和value对应的变量名称相同时,可以只写key名称,如:

var foo = '123';
var obj = {
    foo
};
// 这相当于:
var obj = {
    foo: foo
}

除了属性可以简写,方法也可以简写,即:

var obj = {
    foo() {
        // ...
    }
}
// 相当于
var obj = {
    foo: function() {
        // ...
    }
}

需要注意的是:简写的属性名总是字符串,所以是允许下列这种写法的:

var obj = {
    class() {
        // ...
    }
}

因为它等同于:

var obj = {
    'class': function() {
        // ...
    }
}

如果要表示一个generator函数,则需要在方法名前面加上*

var obj = {
    * gen() {
        // ...
    }
}

ES6中,还允许使用表达式作为属性名,具体方法是使用[]包裹表达式放入属性中,如:

const name = 'some key';
const obj = {
    [name]: 'Hello, world'
}
obj[name]; // 'Hello, world'

除此之外,还可以使用[]定义函数名,如:

const name = 'foo';
const obj = {
    [name]() {
        //...
    }
}
obj.foo();

[]中包裹的内容是一个对象时,会将这个对象转化为string,典型的情况就是转化为[object Object],如:

const key = {};
const obj = {
    [key]: 'test'
};
obj['[object Object]']; // 'test'

当然,转化为string调用的是toString()方法,所以也可以这么做:

const key = {
    toString() {
        return 'name';
    }
};
const obj = {
    [key]: 'RuphiLau'
};
obj.name; // 'RuphiLau'


二、方法的name属性

函数也是一个对象,所以它也拥有属性,而其name属性,返回的则是这个方法名称,如:

const obj = {
    foo(){}
};
obj.foo.name; // 'foo'

如果一个方法使用了settergetter,则它的name属性取不到值,即:

const obj = {
    _realName: '',
    get name() {
        return this._realName;
    },
    set name(nval) {
        this._realName = nval;
    }
};
obj.name.name; // undefined

这时候,可以使用属性描述对象来获取,即:

const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
descriptor.set.name; // 'set name'
descriptor.get.name; // 'get name'

此外,如果使用bind()来得到的函数,其name属性将为:bound 方法名称,如:

(function foo(){}).bind(null).name; // 'bound foo'

而用Function()构造函数得到的函数,其name则为anonymous

(new Function()).name; // 'anonymous'

而如果对象的方法是一个Symbol值,则name属性返回的是这个Symbol值的描述,如:

const key1 = Symbol('desc');
const key2 = Symbol();
const obj = {
    [key1](){},
    [key2](){}
};
obj[key1].name; // '[desc]'
obj[key2].name; // ''


三、Object.is()

由于历史上缺少比较两个同值相等的最佳方法(如==会先转化数据类型后再比较,而即便===,在面对NaN的时候也是没有办法),那么,ES6中引入了Object.is(),有:

Object.is(1, 1); // true
Object.is('str', 'str'); // true
Object.is(NaN, NaN); // true
Object.is({}, {}); // false

而还有一个很重要的结论就是,在Object.is()中,+0-0是不相等的,这是因为Object.is()设计的本质是:同值相等:

Object.is(+0, -0); // false
+0 === -0 ; // true

实现Object.is()的pollyfill可以为:

Object.defineProperty(Object, 'is', {
    value: function(x, y) {
        if (x === y) {
            // 针对+0不等于-0的情况,利用Infinity不等于-Infinity
            return x !== 0 || (1 / x === 1 / y);
        }
        // 针对NaN的情况
        return x !== x && y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});


四、Object.assign()

这个方法可以用于对象的合并,它能够将源对象的所有 可枚举属性,复制到目标对象中,其语法为:

Object.assign(target, source1, source2 [, sourceN]*)

转化与合并策略:
1)当只有target一个参数的时候,直接返回的是target

  • targetnumber/boolean/string类型时,会先转成对象,如:
typeof Object.assign(123); // 'object'
typeof Object.assign(true); // 'object'
  • targetundefined或者null类型时会报错,因为它们无法被转成对象
  • targetstring类型时,会得到一个Array-like对象:
Object.assign('abc');
// { 0: 'a', 1: 'b', 2: 'c', length: 3 }

2)第二个参数起的对象,会被合并到target里,如果存在同名属性,则后面的覆盖前面的

const t = {a:1};
const a = {a:2, b:3};
const b = {b:4, c:5};
const c = {c:6, d:7};
Object.assign(t, a, b, c);
// 将得到:{ a:2, b:4, c:6, d:7 }

3)如果是非源参数(除了target外的参数),如果不能被转为对象,则处理方法是跳过而不会报错。而虽然能够被转为对象,number/boolean类型的参数都不会产生合并效果,而string则可以:

const t = {};
const v1 = 123;
const v2 = false;
const v3 = 'abc';
const v4 = undefined;
const v5 = null;
Object.assign(t, v1, v2, v3, v4, v5);
// {0: "a", 1: "b", 2: "c"}

之所以只有string类型的能被合并,是因为使用Object()构造函数转化为对象的时候,只有string类型的可以产生可枚举对象,而其他类型的只会产生[[PrimitiveValue]]属性,这个属性是不可枚举的,所以不能够被合并到target里:

Object(true); // Boolean {[[PrimitiveValue]]: true}
Object(10); // Number {[[PrimitiveValue]]: 10}
Object('abc'); // String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

此外,Object.assign()执行的是浅拷贝,而非深拷贝,即如果一个属性的值是一个对象,那么拷贝的是它的引用。所以有:

const t = {};
const s = {
    obj: {
        a: 1
    }
};
Object.assign(t, s);
t.obj.a++;
console.log(s.obj.a); // 2

当有嵌套的深对象时,Object.assign()执行的是一层合并,即如果有多层嵌套重名,直接覆盖第一层,如:

const t = {
    obj: {
        a: 1,
        b: 2
    }
};
const s = {
    obj: {
        c: 3
    }
};
Object.assign(t, s);
/*
这种情况下,t对象最终的结果为:
{
    obj: { c: 3 }
}
而非:
{
    obj: { a: 1, b: 2, c: 3 }
}
*/

当遇到数组的时候,Object.assign()也可以处理,需要注意的是:Object.assign()将数组认定为对象,即:

Object.assign([1, 2, 3], [4, 5, 6]);
/*
相当于视为:
Object.assign({0:1, 1:2, 2:3}, {0:4, 1:5, 2:6})
所以会得到:[4, 5, 6]
*/

常见用途

Object.assign()有多种用途,列举如下:
1)为对象添加属性和方法

const t = {/* ... */};
Object.assign(t, {
    a: 1,
    b: 2
});

const Car = function(){}
Object.assign(Car.prototype, {
    showInfo() { /* ... */ },
    addSpeed() { /* ... */ },
    /* ... */
});

2)克隆对象
可以使用以下的方式克隆一个对象,如:

function clone(origin) {
    return Object.assign({}, origin);
}

但是这种方式并不能保持继承链,如果要保持继承链,那么可以像下面这么做:

function clone(origin) {
    const originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
}

3)多个对象的合并
如果想要合并多个对象到源对象,可以这么写:

function mergeToTarget(target, ...source) {
    return Object.assign(target, ...source);
}

如果想要合并多个对象返回一个新对象,可以这么写:

function merge(...source) {
    return Object.assign({}, ...source);
}


五、属性的可枚举性与遍历

1、可枚举性

对象的每个属性都有一个属性描述符(descriptor),可以通过Object.getOwnPropertyDescriptor(对象, 要获取的属性名)来获得,如:

const obj = { foo: 123 }
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
/*
将得到:
{
    configurable: true,
    enumerable: true,
    value: 123,
    writable: true
}
*/

其中,enumerable就表示可枚举性,当为true时,表示该属性可枚举,否则不可枚举。当不可枚举的时候,以下的操作会跳过该属性:

  • for-in 遍历 对象自身 的和 继承的 可枚举属性
  • Object.keys() 得到对象自身所有可枚举属性的键名
  • JSON.stringify() 只串行化对象自身的可枚举属性
  • Object.assign() 忽略不可枚举的属性,只拷贝对象自身的可枚举属性

注意,ES6中规定:所有class的原型方法都是不可枚举的

2、属性的遍历

ES6一共有5种方法可以遍历对象的属性:
1)for-in
遍历对象自身的和可枚举的继承属性(不含Symbol属性)
2)Object.keys()
得到对象自身中所有的可枚举属性(不含Symbol属性)
3)Object.getOwnPropertyNames(obj)
得到对象自身所有属性的key数组(包含不可枚举属性,但是不含Symbol属性)
4)Object.getOwnPropertySymbols(obj)
得到一个数组,包含对象自身的所有Symbol属性
5)Reflect.ownKeys(obj)
得到一个数组,包含对象自身的所有属性的key,包含为Symbol的key名称,也包含不可枚举属性
遍历次序:

  • 首先遍历key为数值的属性,按数值排序
  • 其次遍历key为字符串的属性,按照生成顺序排序
  • 其次遍历key为Symbol值的属性,按照生成顺序排序


六、Object.create()

ES5中新增了Object.create()方法,它的语法为:

Object.create(proto, [ propertiesObject ]);

它可以用来创建一个对象,并将对象的__proto__属性指定为proto,如:

const obj1 = { a: 123 };
const obj2 = Object.create(obj1);
obj2.b = 456;
console.dir(obj2);
/*
得到:
{
    b: 456,
    __proto__: obj1
}
*/

为什么有new运算符了,还需要使用Object.create()呢?这是因为,使用new运算符无法指定对象的__proto__,在实现寄生组合式继承的时候,我们通常采用变通的方法来实现__proto__属性的指定:

const F = function(){};
F.prototype = Parent.prototype;
const childPrototype = new F; // 这样子可以使得childPrototype.__proto__ = F.prototype即Parent.prototype

而现在使用Object.create()的话,寄生组合式继承就很好实现了:

Object.prototype.extends = function(Parent) {
    const Child = this;
    const childPrototype = Object.create(Parent.prototype);
    childPrototype.construtor = Child;
    Child.prototype = childPrototype;
}

此外,Object.create()还可以接收第二个参数,第二个参数和给Object.defineProperties()传入的参数是一样的,如:

const a = {
    name: 'a'
};
const b = Object.create(a, {
    name: {
        configurable: true,
        enumerable: true,
        value: 'b',
        writable: true
    }
});
/*
这种情况下,b就会等于:
{
    name: 'b'
    __proto__: a
}
而a则为:{ name: 'a' }
*/

总结一下:Object.create()创建一个对象,并将新建对象的proto指向第一个参数指定的对象,而第二个参数描述对象指定的属性,则会成为这个新建对象的自身属性。所以可以实现其polyfill为:

if (!Object.create) {
    Object.create = function(proto, propertiesObject) {
        if (!(
            proto === null ||
            typeof proto === 'object' ||
            typeof proto === 'function'
        )) {
            throw TypeError('Argument must be an object, or null');
        }
        var tmp = new Object();
        tmp.__proto__ = proto;
        if (typeof propertiesObject === 'object') {
            Object.defineProperties(tmp, propertiesObject);
        }
    }
} 


七、Object.getOwnPropertyDescriptors()

在ES8中,引入了Object.getOwnPropertyDescriptors(),可以一次性获得某个对象下所有属性的描述对象,如:

const obj = {
    foo: 123,
    get bar() {
        return 'bar';
    }
};
Object.getOwnPropertyDescriptors(obj);
/*
{
    foo: {
        configurable: true,
        enumerable: true,
        value: 123,
        writable: true
    },
    bar: {
        configurable: true,
        enumerable: true,
        get: funtion bar(){ return 'bar'; },
        set: undefined
    }
}
*/

而如果要在ES8前使用这个方法的话,polyfill可以这么实现:

Object.getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
    const result = {};
    for (let key of Reflect.ownKeys(obj)) {
        result[key] = Object.getOwnPropertyDescriptor(obj, key);
    }
    return result;
};

引入这个方法后,我们就可以结合Object.defineProperties()方法来实现拷贝get、set方法,如:

const obj = {
    get foo() {
        return 'foo';
    }
}
const target = {};
Object.defineProperties(target, Object.getOwnPropertyDescriptors(obj));

此外,它还有如下的应用:

1、对象克隆

可以实现浅拷贝的克隆,如:

function shallowClone(obj) {
    return Object.create(
        Object.getPrototypeof(obj),
        Object.getOwnPropertyDescriptors(obj)
    );
}

2、实现mixin(混入)

function mix(object) {
    return {
        with(...mixins) {
            return mixins.reduce(function(prev, mixin) {
                return Object.create(
                    prev,
                    Object.getOwnPropertyDescriptors(mixin)
                );
            }, object);
        }
    };
}

let a = { a: 'a' };
let b = { b: 'b' };
let c = { c: 'c' };
let d = mix(a).with(b, c);

console.log(d);
/*
{
    c: 'c',
    __proto__:
        b: 'b',
        __proto__:
            a: 'a'
}
*/


八、__proto__属性,Object.setPrototypeOf()Object.getPrototypeOf()

1、__proto__属性

__proto__属性用来读取或者设置当前对象的prototype对象,目前几乎所有现代浏览器(包括IE11)都部署了这个属性。

// ES6的写法
const obj = {
    method: function(){ /* ... */ }
}
obj.__proto__ = someOtherObj;

// ES5的写法
const obj = Object.create(someOtherObj);
obj.method = function(){ /* ... */ }

虽然浏览器广泛支持这个属性,ES6里并没有写入正文,而是写在附录中,标准明确规定,只有浏览器必须部署这个属性,而其他运行环境则不一定需要部署。所以,推荐使用以下的方法代替:

  • Object.setPrototypeOf() 写操作
  • Object.getPrototypeOf() 读操作
  • Object.create() 生成操作

2、Object.setPrototypeOf()

Object.setPrototypeOf()方法用来设置一个对象的prototype对象,它是ES6证书推荐的原型设置方法:

Object.setPrototypeOf(obj, prototype);
// 相当于:
function(obj, prototype) {
    obj.__proto__ = prototype;
    return obj
}

如果第一个参数不是对象,则会自动转化为对象,但是由于返回的还是第一个参数,所以操作不会产生任何效果。但是需要注意的是,undefinednull无法转为对象,所以第一个参数如果是undefinednull时,则会报错

3、Object.getPrototypeOf()

用来读取一个对象的原型对象,相当于读取__proto__属性,但是当参数不是对象时,就会读取并返回该对象,而是undefinednull`时,就会报错


九、Object.keys()Object.values()Object.entries()

1、Object.keys()

返回自身的、可枚举的属性的key值数组

2、Object.values()

ES8引入的方法,返回自身的、可枚举的属性的value值数组(不含key为Symbol的键值)
如果参数是一个字符串,会返回各个字符串组成的数组:

Object.values('abc');
// ['a', 'b', 'c']

3、Object.entries()

返回自身的、可枚举的[key: value]对数组(同样跳过key为Symbol类型的属性),常用用法

const obj = {
    one: 1,
    two: 2
}
for (let [key, value] of Object.entries(obj)) {
    console.log(`${key}=${value}`);
}
/*
输出:
one=1
two=2
*/

还可以将对象转为Map结构:

const obj = { foo: 'foo', num: 123 }
const map = new Map(Object.entries(obj));
map; // Map { foo: 'foo', num: 123 }


十、对象的扩展运算符

ES8将扩展运算符引入了对象

1、展开的解构赋值

展开的解构赋值,可以将所有可遍历的、但尚未读取的属性,分配到指定对象上,如:

const {x, y, ...z} = {
    x: 1,
    y: 2,
    a: 3,
    b: 4
};
x; // 1
y; // 2
z; // { a: 3, b: 4 }

解构赋值的右边,应该是一个可以被转成对象的值,如果不能转为对象(如undefinednull),那么解构就会报错,此外还需要注意:
1)展开的解构部分必须是最后一个参数,否则会报错:

const { x, y, ...z } = someObj; // 合法
const { ...x, y, z } = someObj; // 非法
const { x, ...y, ...z } = someObj; // 非法

2)展开的解构赋值的拷贝,是浅拷贝,所以对于复杂数据类型,拷贝的是引用
3)展开的解构赋值不会拷贝继承自原型对象的属性,如:

let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3; // { b: 2 }
o3.a; // undefined

4)展开的解构赋值只能读取 自身的 属性,而不能读取原型对象上的属性,如:

const obj = Object.create({ x: 1, y: 2 });
obj.z = 3;
const { x, ...{ y, z } } = obj;
x; // 1
y; // undefined
z; // 3
/*
obj的结构为:
{
    z: 3,
    __proto__: {
        x: 1,
        y: 2
    }
}
*/

这是因为,x是单纯的解构赋值,会沿着原型链查找,所以可以在原型链上找到,而...{y, z}部分是扩展的解构赋值,我们可以先视为:...resObj,所以得到的resObj
为:

{
    z: 3
}

然后...{ y, z }就相当于:const { y, z } = { z: 3 },从而y为undefined,z为3

2、扩展运算符

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

let z = { a:3, b:4 }
let n = { ...z };
n; // { a:3, b:4 }

这相当于:

let n = Object.assign({}, z);

其他用法:
1)合并两个对象

let n = { ...x, ...y } 
// 相当于
let n = Object.assign({}, x, y);

2)覆盖原有属性
扩展运算符内部的属性,如果在列表中后面存在同名属性,则会被覆盖掉:

let old = { a: 123, b: 456 };
let newObj = { ...old, b: 789 }; // { a: 123, b: 789 }
newObj = { b: 789, ...old }; // { b: 456, a: 123 }

注意:
1)扩展运算符可以接一个表达式,如果表达式的值为null或者undefined,会忽略不会报错
2)如果表达式对象是一个get()函数,则函数会执行:

let obj = {
    ... {
        get name() {
            return 'RuphiLau';
        }
    }
};

obj; // { name: 'RuphiLau' }


十一、Null传导运算符

在业务开发中,经常会有拿到一个JSON,然后取出message.body.user.firstName这种数据的情况,如果其中有个部分是null,则会有问题,所以通常比较安全的写法是:

const firstName = (
    message &&
    message.body &&
    message.body.user &&
    message.body.user.firstName
) || 'default';

为了避免这种层层判断,现在新增了一个提案,引入了Null传导运算符?.简化了写法,当只要其中一部分返回了null或者undefined,整个表达式就返回undefined,所以上例可以改为:

const firstName = message?.body?.user?.firstName || 'default';

用法总结如下:

  • obj?.prop
  • obj?.[expr]
  • func?.(...args)
  • new C?.(...args)