变一个魔术,将这只猫变成一只狗,注意 .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
操作符在解构赋值时可以收集剩余的属性。