对象扩展符简易指南
合并多个 Javascript 对象是常见的工作,但不好的是,至少到目前,Javascript 仍然没有一种方便的方法来完成这个工作。至少现在是这样。
在 ES5 时代,你可以使用 Lodash 的 _.extend(target, [sources]) 方法,而 ES2015 则引入了 Object.assign(target, [sources]) 方法。
幸运的是,对象扩展符 (an ECMASript proposal at stage 3) 是一个很大的进步,提供了简单方便的如下简介方便的语法。
const cat = {
legs: 4, sound: 'meow'};const dog = {
...cat, sound: 'woof'};console.log(dog); // => { legs: 4, sounds: 'woof' }上面的例子中,...cat 复制 cat 的属性到一个新对象 dog 中,cat 中原来的属性 sound 被覆盖,最终值为 woof。
本篇文章将介绍对象 spread 和 rest 语法。包括如何实现对象克隆,对象合并,以及如何覆盖属性值。
下面是关于可枚举属性的简单和概括,以及如何分辨对象自有属性和继承属性。
1. 可枚举以及自有属性
Javascript 里的对象是键值对的组合。
键名通常是一个字符串或者一个 symbol 。值可以是一个原始类型的值(string, boolean, number, undefined 或者 null),一个对象或者一个函数。
下面这个例子将通过对象字面量语法创建一个对象。
const person = {
name: 'Dave',
surname: 'Bowman'};person 这个对象描述了一个人的名和姓氏。
1.1 可枚举属性
描述一个属性有几种描述符,如 writable、enumerable 和configurable 。你可以看这篇文章Object properties in JavaScript了解更多细节。
Enumerable 描述符是一个布尔值,表示这个属性是否可以被枚举。
你可以通过 Object.keys() 方法来枚举一个对象的自有属性和可枚举属性,通过 for..in 语句来枚举所有可枚举的属性。
以对象字面量的形式创建对象 { prop1: 'val1', prop2: 'val2' } 时显式声明的属性都是可枚举的。接下来看看之前创建的 person 对象有哪些可枚举的属性。
const keys = Object.keys(person); console.log(keys); // => ['name', 'surname']
.name 和 .surname 是 person 对象的可枚举属性
接下来是有趣的一部分。对象扩展符复制了原对象的所有可枚举属性。
console.log({ ...person }; // => { name: 'Dave', surname: 'Bowman' }下面我们在 person 对象上创建一个不可枚举属性 .age。
Object.defineProperty(person, 'age', {
enumerable: false, // Make the property non-enumerable
value: 25});console.log(person['age']); // => 25const clone = {
...person
};console.log(clone); // => { name: 'Dave', surname: 'Bowman' }目标对象 clone 从源对象 person 上创建了可枚举属性 .name 和 .surname ,但是不可枚举属性 .age 则被忽略掉。
1.2 自有属性
Javascript 内有原型继承机制,因此一个对象上既有 自有属性,也有从继承属性。
对象字面量显式声明的属性都是自有属性,存在于原型链上的属性都是 继承 属性。
下面将创建一个 personB 对象,并将 person 对象设置成其原型对象。
const personB = Object.create(person, {
profession: { value: 'Astronaut', enumerable: true
}
});console.log(personB.hasOwnProperty('profession')); // => true console.log(personB.hasOwnProperty('name')); // => false console.log(personB.hasOwnProperty('surname')); // => false现在 personB 上有自有的 .profession 属性,以及从原型对象 person 上继承来的 .name 和 .surname 属性。
对象扩展符从源对象上复制自有属性,而会忽略继承的属性
const cloneB = {
...personB
};
console.log(cloneB); // => { profession: 'Astronaut' }...personB 只从 personB 上复制了 .profession 这个自有属性,而继承属性 .name 和 .surname 则被忽略。
总结: 对象扩展符号只会从源对象上复制 自有和可枚举属性,这和 Object.keys() 方法的返回值一样。
2. 对象扩展属性
在对象字面量里,对象扩展符将源对象里的自有属性和可枚举属性复制进目标对象内。
const targetObject = {
...sourceObject, property: 'Value'};顺便说一下,很多时候,对象扩展符与 Object.assign() 等价,上面的代码也可以用这样实现:
const targetObject = Object.assign(
{ },
sourceObject,
{ property: 'Value' }
);一个对象字面量里可以使用多个对象扩展符,与普通的属性声明同时使用:
const targetObject = {
...sourceObject1,
property1: 'Value 1',
...sourceObject2,
...sourceObject3,
property2: 'Value 2'};2.1 对象扩展规则:后面的属性会覆盖前面的
当同时扩展多个对象时,这个对象内可能会存在同名属性,那么最终生成的对象的属性值是怎么计算的,规则很简单:后扩展的属性会覆盖之前扩展的属性。
来看一些简单的例子。下面的代码会实例化一只 cat 。
const cat = {
sound: 'meow',
legs: 4
};现在我们要变一个魔术,将这只猫变成一只狗,注意 .sound 属性值如何变化。
const dog = {
...cat, ...{
sound: 'woof' // `<----- Overwrites cat.sound
}
};
console.log(dog); // =>` { sound: 'woof', legs: 4 }后面声明的 ·woof· 属性值覆盖了前面的在 cat 对象声明的属性值 'meow' , 符合之前所说的规则: 对于同名属性,后声明的值覆盖先声明的值。
这个规则同样适用于对象的初始化
const anotherDog = {
...cat,
sound: 'woof' // `<---- Overwrites cat.sound
};console.log(anotherDog); // =>` { sound: 'woof', legs: 4 }上面代码里,sound: 'woof' 同样覆盖了之前声明的 ' meow' 值。
现在,交换一下扩展对象的位置,输出了不同的结果。
const stillCat = {
...{
sound: 'woof' // `<---- Is overwritten by cat.sound
}, ...cat
};
console.log(stillCat); // =>` { sound: 'meow', legs: 4 }cat 对象仍然是 cat 对象。虽然第一个源对象内的 .sound 属性值是 'woof' ,但是被之后 cat 对象的 'meow' 覆盖。
普通属性和对象扩展的相对位置非常重要,这将直接影响到对象克隆,对象合并,以及填充默认属性的结果。
下面分别详细介绍。
2.2 克隆对象
用对象扩展符克隆一个对象非常简洁,下面的代码克隆了一个 bird 对象。
const bird = {
type: 'pigeon',
color: 'white'};
const birdClone = {
...bird
};
console.log(birdClone); // => { type: 'pigeon', color: 'white' }
console.log(bird === birdClone); // => false...bird 将 bird 对象的自有和可枚举属性复制到目标对象 birdClone 内。
虽然克隆看起来很简单,但仍然要注意其中的几个细微之处。
浅复制
对象扩展只是对对象进行了 浅复制, 只有对象自身被复制,而嵌套的对象结构 没有被复制。
laptop 对象有一个嵌套对象 laptop.screen。现在我们来克隆 laptop对象来看看其内部的嵌套对象怎么变化。
const laptop = { name: 'MacBook Pro', screen: { size: 17, isRetina: true
}};const laptopClone = {
...laptop};console.log(laptop === laptopClone); // => false console.log(laptop.screen === laptopClone.screen); // => true第一个比较语句 laptop === laptopClone 的值为 false, 说明主对象被正确克隆。
然而 laptop.screen === laptopClone.screen 的计算结果为 true ,说明 laptopClone.screen 没有被复制,而是 laptop.screen 和 laptopClone.screen 引用了同一个嵌套对象。
好的一点是,你可以在对象的任何一层使用对象扩展符,只需要再多做一点工作就同样可以克隆一个嵌套对象。
const laptopDeepClone = {
...laptop, screen: {
...laptop.screen
}
};
console.log(laptop === laptopDeepClone); // => false console.log(laptop.screen === laptopDeepClone.screen); // => false使用 ...laptop.screen 使嵌套对象也被克隆,现在 laptopDeepClone 完全克隆了 laptop。
原型失去了
下面的代码声明了一个 Game 类,并创造了一个 doom实例。
class Game {
constructor(name) { this.name = name;
}
getMessage() { return `I like ${this.name}!`;
}
}const doom = new Game('Doom');
console.log(doom instanceof Game); // => true console.log(doom.name); // => "Doom" console.log(doom.getMessage()); // => "I like Doom!"现在我们克隆一个通过构造函数创建的 doom 实例,结果可能与你想的不同。
const doomClone = {
...doom
};console.log(doomClone instanceof Game); // => false console.log(doomClone.name); // => "Doom" console.log(doomClone.getMessage());
// TypeError: doomClone.getMessage is not a function...doom 将自有属性 .name 属性复制到 doomClone 内。
doomClone 现在只是一个普通的 JavaScript 对象,它的原型是 Object.prototype 而不是预想中的Game.prototype。对象扩展不保留源对象的原型。
因此调用 doomClone.getMessage() 方法会抛出一个 TypeError 错误,因此 doomClone 没有继承 getMessage() 方法。
当然我们可以手动在克隆对象上加上 __proto__ 属性来结局这个问题。
const doomFullClone = {
...doom, __proto__: Game.prototype
};console.log(doomFullClone instanceof Game); // => true console.log(doomFullClone.name); // => "Doom" console.log(doomFullClone.getMessage()); // => "I like Doom!"对象字面量内部的 __proto__ 属性确保了 doomFullClone 的原型为 Game.prototype。
尽量不要尝试这种方法。__proto__ 属性已经废弃,这里使用它只是为了论证前面的观点。
对象扩展的目的是以浅复制的方式扩展自有和可枚举属性,因此不保留源对象的原型似乎也说得过去。
例外,这里用 Object.assign() 来克隆 doom 更加合理。
const doomFullClone = Object.assign(new Game(), doom);console.log(doomFullClone instanceof Game); // => true console.log(doomFullClone.name); // => "Doom" console.log(doomFullClone.getMessage()); // => "I like Doom!"
这样,就保留了原型。
2.3 不可变对象更新
在一个应用里,同一个对象可能会用于多个地方,直接修改这个对象会带来意想不到的副作用,并且追踪这个修改及其困难。
一个好的方式是使操作不可变。不可变性使修改对象更为可控,更有利于书写。pure functions。即时是在复杂的应用场景,由于单向数据流,更容易确定对象的来源和改变的原因。
使用对象扩展能更方便的以不可变方式来修改一个对象。假设现在你有一个对象来描述一本书的信息。
const book = {
name: 'JavaScript: The Definitive Guide',
author: 'David Flanagan',
edition: 5,
year: 2008
};现在,书第六版即将出版,我们用对象扩展的处理这个场景。
const newerBook = {
...book,
edition: 6, // <----- Overwrites book.edition
year: 2011 // <----- Overwrites book.year
};
console.log(newerBook);
/*
{
name: 'JavaScript: The Definitive Guide',
author: 'David Flanagan',
edition: 6,
year: 2011
}
*/newerBook 对象内的 ...book 扩展了 book 对象的属性。手动创建的可枚举属性 editon: 6 和 year: 2011 更新了原有的同名属性。
重要的属性一般在末尾来指定,以便覆盖前面已经创建的同名属性。
newerBook 是一个更新了某些属性的新的对象,并且我们没有改变原有的 book 对象,满足了不可变性的要求。
2.4 合并对象
使用对象扩展符合并多个对象非常简单。
现在我们合并3个对象来创建一个“合成对象”。
const part1 = {
color: 'white'};const part2 = {
model: 'Honda'};const part3 = {
year: 2005};const car = {
...part1,
...part2,
...part3
};
console.log(car); // { color: 'white', model: 'Honda', year: 2005 }上面的例子中,我们使用 part1、part2、part3 3个对象合并成了一个 car 对象。
另外,不要忘了之前讲的规则,后面的属性值会覆盖前面的同名属性值。这是我们合并有同名属性对象的计算依据。
现在我们稍微改变一下之前的代码。给 part1 和 part3 增加一个 .configuration 属性。
const part1 = {
color: 'white',
configuration: 'sedan'};const part2 = {
model: 'Honda'};const part3 = {
year: 2005,
configuration: 'hatchback'};const car = {
...part1,
...part2,
...part3 // <--- part3.configuration overwrites part1.configuration};
console.log(car);
/*
{
color: 'white',
model: 'Honda',
year: 2005,
configuration: 'hatchback' `<--- part3.configuration
}
*/...part1 将 configuration 属性设置成了 'sedan'。然而之后的扩展符 ...part3 覆盖了之前的同名 .configuration,最终生成的对象值为 'hatchback'。
2.5 给对象设置默认值
一个对象在程序运行时可能会有多套不同的属性值,有些属性可能会被设置,有些则可能被忽略。
这种情况通常发生在一个配置对象上。用户可以指定一个重要的属性值,不重要的属性则使用默认值。
现在我们来实现一个 multline(str, config) 方法,将str 按照给定的长度分割成多行。
config 对象接受下面3个可选的参数。
width: 分割的字符长度,默认是10。newLine: 添加到每一行结尾的的字符, 默认是\n。indent: 每一行开头的缩进符,默认是空字符串''。
下面是一些 multline() 运行的例子。
multiline('Hello World!');
// =>` 'Hello Worl\nd!'multiline('Hello World!', { width: 6 });
// => 'Hello \nWorld!'multiline('Hello World!', { width: 6, newLine: '*' });
// => 'Hello *World!'multiline('Hello World!', { width: 6, newLine: '*', indent: '_' });
// => '_Hello *_World!'config 参数接受几套不同的属性值:你可以指定1,2或者3个属性值,甚至不指定任何一个属性。
使用对象扩展语法来填充配置对象非常简单,在对象字面量里,首先扩展默认值对象,然后是配置对象,如下所示:
function multiline(str, config = {}) {
const defaultConfig = { width: 10, newLine: '\n', indent: ''
}; const safeConfig = {
...defaultConfig,
...config
}; let result = ''; // Implementation of multiline() using
// safeConfig.width, safeConfig.newLine, safeConfig.indent
// ...
return result;
}我们来仔细了解一下 safeConfig 对象。
...defaultConfig 首先将默认对象的属性复制,随后,...config 里用户自定义的值覆盖了之前的默认属性值。
这样 safeConfig 值就拥有了所有 multiline() 需要的配置参数。无论调用 multiline() 函数时,输入的 config 是否缺失了某些属性,都可以保证 safeConfig 拥有所有的必备参数。
显而易见,对象扩展实现了我们想要的 给对象设置默认值。
2.6 更加深入
对象扩展更有用的一点是用于嵌套对象,当更新一个复杂对象时,更具有可读性,比 Object.assign() 更值得推荐。
下面的 box 对象定义一个盒子及盒子内的物品。
const box = {
color: 'red', size: { width: 200,
height: 100
},
items: ['pencil', 'notebook']
};box.size 描述了这个盒子的尺寸,box.items 列举了盒子内的物品。
为了使盒子看起来更高,我们增大 box.size.height 的值,只需要在嵌套对象上使用 对象扩展符。
const biggerBox = {
...box, size: {
...box.size, height: 200
}
};
console.log(biggerBox);
/*
{
color: 'red',
size: {
width: 200,
height: 200 <----- Updated value
},
items: ['pencil', 'notebook']
}
*/...box 确保了 biggerBox 获得了 源对象 box 上的全部属性。
更新 box.size 的 height 值需要额外一个 {...box.size, height: 200} 对象,该对象接收 box.size 的全部属性,并将 height 值更新至 200。
只需要一个语句就能更新对象的多处属性。
现在如果我们还想把颜色改成 black ,增加盒子的宽度到 400, 并且再放一把尺子到盒子内,应该怎么办?同样很简单。
const blackBox = {
...box, color: 'black', size: {
...box.size, width: 400
},
items: [
...box.items, 'ruler'
]
};
console.log(blackBox);
/*
{
color: 'black', <----- Updated value
size: {
width: 400, <----- Updated value
height: 100
},
items: ['pencil', 'notebook', 'ruler'] `<----- A new item ruler
}
*/2.7 扩展 undefined、null 和 原始类型值
如果在 undefined、null 和 原始类型值 上使用原始类型的值,不会复制任何属性,也不会抛出错误,只是简单的返回一个空对象。
const nothing = undefined;
const missingObject = null;
const two = 2;console.log({ ...nothing }); // => { } console.log({ ...missingObject }); // => { } console.log({ ...two }); // => { }如上所示:从 nothing, missingObject 和 two不会复制任何属性。
当然,这只是一个演示,毕竟根本没有理由在一个原始类型的值上面使用对象扩展符。
3. 剩余属性
当使用解构赋值将对象的属性值赋值给变量后,剩余的属性值将会被集合进一个剩余对象内。
下面的代码演示了怎么使用 rest 属性。
const style = {
width: 300,
marginLeft: 10,
marginRight: 30};const { width, ...margin } = style;
console.log(width); // => 300 console.log(margin); // => { marginLeft: 10, marginRight: 30 }通过解构赋值,我们定义了一个新的变量 width ,并将它的值设置为 style.width。而解构赋值声明内的 ...margin 则获得了 style 对象的其余属性,margin 对象获取了 marginLeft 和 marginRight 属性。
rest 操作符同样只会获取自有属性和可枚举属性。
注意,在解构赋值内,rest 操作符只能放到最后,因此 const { ...margin , width } = style 无效,并会抛出一个 SyntaxError: Rest element must be last element 错误。
4. 结论
对象扩展需要以下几点:
它只会提取对象的自有属性和可枚举属性
后定义的属性值会覆盖之前定义过的同名属性值
同时,对象扩展使用上方便简洁,能更好的处理嵌套对象,保持不可变性,在实现对象克隆和填充默认属性值上也使用方便。
而 rest 操作符在解构赋值时可以收集剩余的属性。
共同学习,写下你的评论
评论加载中...
作者其他优质文章