对象字面量语法的扩展
属性初始化器的速记法
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。
const foo = 'bar';
const baz = { foo };
baz; // {foo: "bar"}
// 等同于
const baz = { foo: foo };
2
3
4
5
6
function f(x, y) {
return { x, y };
}
// 等同于
function f(x, y) {
return { x: x, y: y };
}
f(1, 2); // Object {x: 1, y: 2}
2
3
4
5
6
7
8
9
10
11
方法简写
除了属性简写,方法也可以简写。
const o = {
method() {
return 'Hello!';
}
};
// 等同于
const o = {
method: function() {
return 'Hello!';
}
};
2
3
4
5
6
7
8
9
10
11
12
13
let birth = '2000/01/01';
const Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() {
console.log('我的名字是', this.name);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
CommonJS 模块输出一组变量,就非常合适使用简洁写法。
let 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
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
如果某个方法的值是一个 Generator 函数,前面需要加上星号。
const obj = {
*m() {
yield 'hello world';
}
};
2
3
4
5
注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。class 是字符串,所以不会因为它属于关键字,而导致语法解析报错。
const obj = {
class() {}
};
// 等同于
var obj = {
class: function() {}
};
2
3
4
5
6
7
8
9
需计算属性名
但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用标识符定义属性。
var obj = {
foo: true,
abc: 123
};
2
3
4
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
2
3
4
5
6
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word']; // "hello"
a[lastWord]; // "world"
a['last word']; // "world"
2
3
4
5
6
7
8
9
10
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
2
3
4
5
6
7
8
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。
const keyA = { a: 1 };
const keyB = { b: 2 };
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject; // Object {[object Object]: "valueB"}
2
3
4
5
6
7
8
9
重复的对象字面量属性
ES5 严格模式为重复的对象字面量属性引入了一个检查,若找到重复的属性名,就会抛出错误。例如,以下代码就有问题。
但 ES6 移除了重复属性的检查,严格模式与非严格模式都不再检查重复的属性。当存在重复属性时,排在后面的属性的值会成为该属性的实际值,如下所示:
'use strict';
var person = {
name: 'Nicholas',
name: 'Greg' //在ES6严格模式中不会出错
};
console.log(person.name); // "Greg"
2
3
4
5
6
7
8
属性的可枚举性和遍历
可枚举性
描述对象的 enumerable 属性,称为”可枚举性“,如果该属性为 false,就表示某些操作会忽略当前属性。 目前,有四个操作会忽略 enumerable 为 false 的属性。
- for...in 循环:只遍历对象自身的和继承的可枚举的属性。
- Object.keys():返回对象自身的所有可枚举的属性的键名。
- JSON.stringify():只串行化对象自身的可枚举的属性。
- Object.assign(): 忽略 enumerable 为 false 的属性,只拷贝对象自身的可枚举的属性。
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable;
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable;
// false
2
3
4
5
ES6 规定,所有 Class 的原型的方法都是不可枚举的。
Object.getOwnPropertyDescriptor(
class {
foo() {}
}.prototype,
'foo'
).enumerable;
// false
2
3
4
5
6
7
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用 for...in 循环,而用 Object.keys()代替。
属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性。
for...in
for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
Object.keys(obj)
Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
Reflect.ownKeys(obj)
Reflect.ownKeys 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
自有属性的枚举顺序
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0 });
// ['2', '10', 'b', 'a', Symbol()]
2
super 关键字
ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find(); // "hello
2
3
4
5
6
7
8
9
10
11
12
13
注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
// 报错
const obj = {
foo: super.foo
};
// 报错
const obj = {
foo: () => super.foo
};
// 报错
const obj = {
foo: function() {
return super.foo;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面三种 super 的用法都会报错,因为对于 JavaScript 引擎来说,这里的 super 都没有用在对象的方法之中。第一种写法是 super 用在属性里面,第二种和第三种写法是 super 用在一个函数里面,然后赋值给 foo 属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
super 是指向当前对象的原型的一个指针,实际上就是 Object.getPrototypeOf(this)的值。
const proto = {
x: 'hello',
foo() {
console.log(this.x);
}
};
const obj = {
x: 'world',
foo() {
super.foo();
}
};
Object.setPrototypeOf(obj, proto);
obj.foo(); // "world"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
正式的“方法”定义
在 ES6 之前,“方法”的概念从未被正式定义,它此前仅指对象的函数属性(而非数据属性)。ES6 则正式做出了定义:方法是一个拥有[[Homeobject]]内部属性的函数,此内部属性指向该方法所属的对象。
任何对 super 的引用都会使用[[Homeobject]]属性来判断要做什么。第一步是在[[Homeobject]]上调用 Object.getPrototypeof()来获取对原型的引用;接下来,在该原型上查找同名函数;最后,创建 this 绑定并调用该方法。这里有个例子:
let person = {
getGreeting() {
return 'Hello';
}
};
// 原型为 person
let friend = {
getGreeting() {
return super.getGreeting() + ', hi!';
}
};
Object.setPrototypeOf(friend, person);
console.log(friend.getGreeting()); //"Hello, hi!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
调用 friend.getsreeting()返回了一个字符串,也就是 person.getoreeting()的返回值与",hi!”的合并结果。此时 friend.getGreeting()的[[Homeobject]]值是 friend,并且 friend 的原型是 person,因此 super.getGreeting()就等价于 person.getGreeting.call(this)。
对象的新增方法
ES 从 ES5 开始就有一个设计意图:避免创建新的全局函数,避免在 object 对象的原型上添加新方法,而是尝试寻找哪些对象应该被添加新方法。因此,对其他对象不适用的新方法就被添加到全局的 Object 对象上。ES6 在 Object 对象上引入了两个新方法:Object.is()方法和 Object.assign()方法,以便让特定任务更易完成。
Object .is()真正的两个值是否相等
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及+0 等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo');
// true
Object.is({}, {});
// false
2
3
4
不同之处只有两个:一是+0 不等于-0,二是 NaN 等于自身。
+0 === -0; //true
NaN === NaN; // false
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
2
3
4
5
ES5 可以通过下面的代码,部署 Object .is()。
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
});
2
3
4
5
6
7
8
9
10
11
12
13
Object.assign()用于对象的合并
混入(Mixin)是在 JS 中组合对象时最流行的模式。在一次混入中,一个对象会从另一个对象中接收属性与方法。很多 JS 的库中都有类似下面的混入方法:
function mixin(receiver, supplier) {
Object.keys(supplier).forEach(function(key) {
receiver[key] = supplier[key];
});
return receiver;
}
2
3
4
5
6
7
Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target; // {a:1, b:2, c:3}
2
3
4
5
6
7
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target; // {a:1, b:2, c:3}
2
3
4
5
6
7
如果该参数不是对象,则会先转成对象,然后返回。
typeof Object.assign(2); // "object"
由于 undefined 和 null 无法转成对象,所以如果它们作为参数,就会报错。
Object.assign(undefined); // 报错
Object.assign(null); // 报错
2
如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果 undefined 和 null 不在首参数,就不会报错。
let obj = { a: 1 };
Object.assign(obj, undefined) === obj; // true
Object.assign(obj, null) === obj; // true
2
3
属性名为 Symbol 值的属性,也会被 Object.assign 拷贝。
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' });
// { a: 'b', Symbol(c): 'd' }
2
Object.assign 可以用来处理数组,但是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5]);
// [4, 5, 3]
2
取值函数的处理
const source = {
get foo() {
return 1;
}
};
const target = {};
Object.assign(target, source);
// { foo: 1 }
2
3
4
5
6
7
8
9
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
2
3
4
5
6
Object(true); // {[[PrimitiveValue]]: true}
Object(10); // {[[PrimitiveValue]]: 10}
Object('abc'); // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
2
3
上面代码中,布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面,这个属性是不会被 Object.assign 拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。
Object.assign 拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
Object.assign(
{ b: 'c' },
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
);
// { b: 'c' }
2
3
4
5
6
7
8
常见用途
为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, { x, y });
}
}
2
3
4
5
为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
克隆对象
function clone(origin) {
return Object.assign({}, origin);
}
2
3
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
2
3
4
合并多个对象
将多个对象合并到某个对象。
const merge = (target, ...sources) => Object.assign(target, ...sources);
如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。
const merge = (...sources) => Object.assign({}, ...sources);
为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
2
3
4
5
6
7
8
9
10
注意,由于存在浅拷贝的问题,DEFAULTS 对象和 options 对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS 对象的该属性很可能不起作用。
const DEFAULTS = {
url: {
host: 'example.com',
port: 7070
}
};
processContent({ url: { port: 8000 } });
// {
// url: {port: 8000}
// }
2
3
4
5
6
7
8
9
10
11
Object.getOwnPropertyDescriptors()返回所有自身属性(非继承属性)的描述对象
ES5 的 Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了 Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
const obj = {
foo: 123,
get bar() {
return 'abc';
}
};
Object.getOwnPropertyDescriptors(obj);
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
模拟一个:
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
2
3
4
5
6
7
该方法的引入目的,主要是为了解决 Object.assign()无法正确拷贝 get 属性和 set 属性的问题。
const source = {
set foo(value) {
console.log(value);
}
};
const target1 = {};
Object.assign(target1, source);
Object.getOwnPropertyDescriptor(target1, 'foo');
// { value: undefined,
// writable: true,
// enumerable: true,
// configurable: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,source 对象的 foo 属性的值是一个赋值函数,Object.assign 方法将这个属性拷贝给 target1 对象,结果该属性的值变成了 undefined。这是因为 Object.assign 方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。
这时,Object.getOwnPropertyDescriptors()方法配合 Object.defineProperties()方法,就可以实现正确拷贝。
const source = {
set foo(value) {
console.log(value);
}
};
const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo');
// { get: undefined,
// set: [Function: set foo],
// enumerable: true,
// configurable: true }
2
3
4
5
6
7
8
9
10
11
12
13
Object.setPrototypeOf()设置对象的原型对象
__proto__属性(前后各两个下划线),用来读取或设置当前对象的 prototype 对象。目前,所有浏览器(包括 IE11)都部署了这个属性。
该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的 Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
Object.setPrototypeOf 方法的作用__proto__相同,用来设置一个对象的 prototype 对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
// 格式
Object.setPrototypeOf(object, prototype);
// 用法
const o = Object.setPrototypeOf({}, null);
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x; // 10
obj.y; // 20
obj.z; // 40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。
Object.setPrototypeOf(1, {}) === 1; // true
Object.setPrototypeOf('foo', {}) === 'foo'; // true
Object.setPrototypeOf(true, {}) === true; // true
2
3
由于 undefined 和 null 无法转为对象,所以如果第一个参数是 undefined 或 null,就会报错。
Object.getPrototypeOf()读取对象的原型对象
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype;
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype;
// false
2
3
4
5
6
7
8
9
10
11
12
如果参数不是对象,会被自动转为对象。
// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1);
// Number {[[PrimitiveValue]]: 0}
// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo');
// String {length: 0, [[PrimitiveValue]]: ""}
// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true);
// Boolean {[[PrimitiveValue]]: false}
Object.getPrototypeOf(1) === Number.prototype; // true
Object.getPrototypeOf('foo') === String.prototype; // true
Object.getPrototypeOf(true) === Boolean.prototype; // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果参数是 undefined 或 null,它们无法转为对象,所以会报错。
Object.keys()返回自身属性的键名的数组
ES5 引入了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj);
// ["foo", "baz"]
2
3
ES2017 引入了跟 Object.keys 配套的 Object.values 和 Object.entries,作为遍历一个对象的补充手段,供 for...of 循环使用。
let { keys, values, entries } = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.values()返回自身属性的键值的数组
Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
const obj = { foo: 'bar', baz: 42 };
Object.values(obj);
// ["bar", 42]
2
3
Object.values 只返回对象自身的可遍历属性。
const obj = Object.create({}, { p: { value: 42 } });
Object.values(obj); // []
2
Object.values 会过滤属性名为 Symbol 值的属性。
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']
2
如果 Object.values 方法的参数是一个字符串,会返回各个字符组成的一个数组。
Object.values('foo');
// ['f', 'o', 'o']
2
如果参数不是对象,Object.values 会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values 会返回空数组。
Object.values(42); // []
Object.values(true); // []
2
Object.entries()返回自身属性的键值对数组
Object.entries()方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj);
// [ ["foo", "bar"], ["baz", 42] ]
2
3
let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);
}
// "one": 1
// "two": 2
2
3
4
5
6
Object.entries 方法的另一个用处是,将对象转为真正的 Map 结构。
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map; // Map { foo: "bar", baz: 42 }
2
3
自己实现 Object.entries 方法,非常简单。
// Generator函数的版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// 非Generator函数的版本
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.fromEntries()将键值对数组转为对象
Object.fromEntries([['foo', 'bar'], ['baz', 42]]);
// { foo: "bar", baz: 42 }
2
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
// 例一
const entries = new Map([['foo', 'bar'], ['baz', 42]]);
Object.fromEntries(entries);
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map);
// { foo: true, bar: false }
2
3
4
5
6
7
8
9
10
该方法的一个用处是配合 URLSearchParams 对象,将查询字符串转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'));
// { foo: "bar", baz: "qux" }
2
参考文章ECMAScript 6 入门