JavaScript ES7 对象展开运算

ES6ES7 中提供了多种新的语法,可以更方便的操作一个对象。

属性简洁表示法

定义对象时直接写入变量和函数,作为对象的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = "ccf";
var age = 26;
var user = {
name,
age
};
// 等同于
/ *
var user = {
name: name,
age: age
}; */
console.log(user); // { name: 'ccf', age: 26 }

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

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
method() {
return "Hello!";
}
};
// 等同于
var o = {
method: function() {
return "Hello!";
}
};

下面是一个例子:

1
2
3
4
5
6
7
8
9
var birth = '2000/01/01';
var Person = {
name: '张三',
// 等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};

几个应用场景:

CommonJS 模块输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};

属性赋值器(setter)和取值器(getter)

1
2
3
4
5
6
7
8
9
10
11
var cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
this._wheels = value;
}
}

注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。

1
2
3
4
5
6
7
8
var obj = {
class () {}
};
// 等同于
var obj = {
'class': function() {}
};

上面代码中,class 是字符串,所以不会因为它属于关键字,而导致语法解析报错。

如果某个方法的值是一个 Generator 函数,前面需要加上星号。

1
2
3
4
5
var obj = {
* m(){
yield 'hello world';
}
};

属性名表达式

读取属性

1
2
3
4
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
  • 方法一是直接用 标识符 作为属性名。
  • 方法二是用 表达式 作为属性名,这时要将表达式放在方括号之内。

定义属性

如果使用字面量方式定义对象(使用大括号 {} ),在 ES5 中只能使用方法一(标识符)定义属性。

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

1
2
3
4
5
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

表达式也可以用于定义方法名。

1
2
3
4
5
6
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi

注意:属性名表达式与简洁表示法,不能同时使用,会报错。

1
2
3
4
5
6
7
8
// 报错
var foo = 'bar';
var bar = 'abc';
var baz = { [foo] };
// 正确
var foo = 'bar';
var baz = { [foo]: 'abc'};

属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

1
2
3
4
5
6
7
8
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}

上面代码中,[keyA]和[keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而 myObject 最后只有一个[object Object]属性。

方法的 name 属性

函数的 name 属性,返回函数名。对象方法也是函数,因此也有 name 属性。

1
2
3
4
5
6
7
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"

如果对象的方法使用了取值函数(getter)和存值函数(setter),则 name 属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。

1
2
3
4
5
6
7
8
9
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name // TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

有两种特殊情况:bind 方法创造的函数,name 属性返回 bound + 原函数的名字Function 构造函数创造的函数,name属性返回anonymous

1
2
3
4
5
(new Function()).name // "anonymous"
var doSomething = function() {
};
doSomething.bind().name // "bound doSomething"

如果对象的方法是一个 Symbol 值,那么 name 属性返回的是这个 Symbol 值的描述。

1
2
3
4
5
6
7
8
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""

key1 对应的 Symbol 值有描述,key2 没有。

Object.js()

ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

ES6 提出 “Same-value equality”(同值相等)算法,用来解决这个问题。Object.is() 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

1
2
Object.is('foo', 'foo') // true
Object.is({}, {}) // false

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

1
2
3
4
5
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

ES5 可以通过下面的代码,部署 Object.is()

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});

Object.assign()

Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),该方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

传入的第一个参数有如下几点注意:

  • 如果只有一个参数,Object.assign 会直接返回该参数。
  • 如果该参数不是对象,则会先转成对象,然后返回。
  • 由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
  • 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。
  • 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

注意点

Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

1
2
3
4
5
var obj1 = {a: {b: 1}};
var obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2

源对象 obj1 的a属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

对于这种嵌套的对象,一旦遇到同名属性,Object.assign 的处理方法是替换,而不是添加。

1
2
3
4
var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'hello' } }
Object.assign(target, source) // { a: { b: 'hello' } }
// target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: 'hello', d: 'e' } }的结果。

有一些函数库提供 Object.assign 的定制版本(比如Lodash_.defaultsDeep方法),可以解决浅拷贝的问题,得到深拷贝的合并。

注意,Object.assign可以用来处理数组,但是会把数组视为对象。

1
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]

Object.assign把数组视为属性名为0、1、2的对象,因此源数组的0号属性4覆盖了目标数组的0号属性1。

用途

  • 为对象添加属性
1
2
3
4
5
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
  • 为对象添加方法
1
2
3
4
5
6
7
8
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
}
});
// 使用简洁语法直接将函数放到 {} 中,然后使用 assign 将函数添加给 SomeClass
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
};
  • 克隆对象
1
2
3
function clone(origin) {
return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

1
2
3
4
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
  • 合并多个对象

将多个对象合并到某个对象。

1
const merge = (...sources) => Object.assign({}, ...sources);
  • 为属性指定默认值
1
2
3
4
5
6
7
8
9
10
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}

注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。

1
2
3
4
5
6
7
8
9
10
11
const DEFAULTS = {
url: {
host: 'example.com',
port: 7070
},
};
processContent({ url: {port: 8000} })
// {
// url: {port: 8000}
// }

上面代码的原意是将 url.port 改成8000,url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。

Link