在 ES6、ES7 中提供了多种新的语法,可以更方便的操作一个对象。
属性简洁表示法
定义对象时直接写入变量和函数,作为对象的属性和方法。
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, 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()
|
注意:属性名表达式与简洁表示法,不能同时使用,会报错。
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
|
上面代码中,[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
|
如果对象的方法使用了取值函数(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 const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo'); descriptor.get.name descriptor.set.name
|
有两种特殊情况:bind 方法创造的函数,name 属性返回 bound + 原函数的名字;Function 构造函数创造的函数,name属性返回anonymous。
1 2 3 4 5
| (new Function()).name var doSomething = function() { }; doSomething.bind().name
|
如果对象的方法是一个 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 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') Object.is({}, {})
|
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
1 2 3 4 5
| +0 === -0 NaN === NaN Object.is(+0, -0) Object.is(NaN, NaN)
|
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) { return x !== 0 || 1 / x === 1 / y; } 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
|
源对象 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])
|
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) { } }); 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.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。
Link