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