转载自品略图书馆 http://www.pinlue.com/article/2019/10/2516/299738779449.html
为了保证的可读性,本文采用意译而非直译。
在ES5中,咱们合并对象通常使用Lodash的 _.extend(target,[sources])
方法,在ES6中咱们使用 Object.assign(target,[sources])
来合并对象,当然现在最常用应该是使用 Rest/Spread
(展开运算符与剩余操作符)。
来个例子:
const cat = {
legs: 4,
sound: "meow"
};
const dog = {
...cat,
sound: "woof"
};
console.log(dog); // => { legs: 4, sounds: "woof" }
在上面的示例中, ...cat
将 cat
的属性复制到新的对象 dog
中, .sound
属性接收最终值 "woof"
。
本文将介绍对象 spread
和 rest
语法,包括对象传播如何实现对象克隆、合并、属性覆盖等方法。
下面简要介绍一下可枚举属性,以及如何区分自有属性和继承属性。这些是理解对象 spread
和 rest
工作原理的必要基础。
1.属性描述对象
JS 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。
下面是属性描述对象的一个例子。
{
value: 123,
writable: false,
enumerable: true,
configurable: false,
get: undefined,
set: undefined
}
属性描述对象提供6个元属性。
(1)value
value
是该属性的属性值,默认为undefined。
(2)writable
writable
是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true。
(3)enumerable
enumerable
是一个布尔值,表示该属性是否可遍历,默认为 true
。如果设为 false
,会使得某些操作(比如 for...in
循环、 Object.keys()
)跳过该属性。
(4)configurable
configurable
是一个布尔值,表示可配置性,默认为 true
。如果设为 false
,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象( value
属性除外)。也就是说, configurable
属性控制了属性描述对象的可写性。
(5)get
get
是一个函数,表示该属性的取值函数( getter
),默认为 undefined
。
(6)set
set
是一个函数,表示该属性的存值函数( setter
),默认为 undefined
。
2.可枚举和自有属性
JS中的对象是键和值之间的关联。 键
类型通常是字符串或 symbol
。 值
可以是基本类型(string、boolean、number、undefined或null)、对象或函数。
下面使用对象字面量来创建对象:
const person = {
name: "Dave",
surname: "Bowman"
};
2.1 可枚举的属性
enumerable
属性是一个布尔值,它表示在枚举对象的属性时该属性是否可访问。
咱们可以使用 object.keys()
(访问自有和可枚举的属性)枚举对象属性,例如,在 for..in
语句中(访问所有可枚举属性)等等。
在对象字面量 {prop1:"val1",prop2:"val2"}
中显式声明的属性是可枚举的。来看看 person
对象包含哪些可枚举属性:
const keys = Object.keys(person);
console.log(keys); // => ["name", "surname"]
.name
和 .surname
是 person
对象的可枚举属性。
接下来是有趣的部分, 对象展开来自源可枚举属性的副本:
onsole.log({ ...person };// => { name: "Dave", surname: "Bowman" }
现在,在 person
对象上创建一个不可枚举的属性 .age
。然后看看展开的行为:
Object.defineProperty(person, "age", {
enumerable: false, // 让属性不可枚举
value: 25
})
console.log(person["age"]); // => 25
const clone = {
...person
};
console.log(clone); // => { name: "Dave", surname: "Bowman" }
.name
和 .surname
可枚举属性从源对象 person
复制到 clone
,但是不可枚举的 .age
被忽略了。
2.2 自有属性
JS包含原型继承。因此,对象属性既可以是自有的,也可以是继承的。
在对象字面量显式声明的属性是自有的。但是对象从其原型接收的属性是继承的。
接着创建一个对象 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
对象具有自己的属性 .professional
,并从原型 person
继承 .name
和 .surname
属性。
展开运算只展开自有属性,忽略继承属性。
const cloneB = {
...personB
};
console.log(cloneB); // => { profession: "Astronaut" }
对象展开 ...personB
只从源对象 personB
复制,继承的 .name
和 .surname
被忽略。
3. 对象展开属性
对象展开语法从源对象中提取自有和可枚举的属性,并将它们复制到目标对象中。
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"
};
3.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
”覆盖了前面的值“ meow
”(来自 cat
源对象)。这与后一个属性使用相同的键覆盖最早的属性的规则相匹配。
相同的规则适用于对象初始值设定项的常规属性:
const anotherDog = {
...cat,
sound: "woof" // <---- Overwrites cat.sound
};
console.log(anotherDog); // => { sound: "woof", legs: 4 }
现在,如果您交换展开对象的相对位置,结果会有所不同:
const stillCat = {
...{
sound: "woof" // <---- Is overwritten by cat.sound
},
...cat
};
console.log(stillCat); // => { sound: "meow", legs: 4 }
对象展开中,属性的相对位置很重要。展开语法可以实现诸如对象克隆,合并对象,填充默认值等等。
3.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
对中。因此, birdClone
是 bird
的克隆。
3.3 浅拷贝
对象展开执行的是对象的浅拷贝。仅克隆对象本身,而不克隆嵌套对象。
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
,这意味着 laptop.screen
和 laptopClone.screen
引用了相同对象。
当然可以在嵌套对象使用展开属性,这样就能克隆嵌套对象。
const laptopDeepClone = {
...laptop,
screen: {
...laptop.screen
}
};
console.log(laptop === laptopDeepClone); // => false
console.log(laptop.screen === laptopDeepClone.screen); // => false
3.4 原型丢失
下面的代码片段声明了一个类 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
是一个普通的JS对象,原型是 Object.prototype
,但不是 Game.prototype
。所以对象展开不保留源对象的原型。
因此,调用 doomClone.getMessage()
会抛出一个类型错误,因为 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!"
3.5 不可变对象更新
当在应用程序的许多位置共享同一对象时,对其进行直接修改可能会导致意外的副作用。追踪这些修改是一项繁琐的工作。
更好的方法是使操作不可变。不变性保持在更好的控制对象的修改和有利于编写纯函数。即使在复杂的场景中,由于数据流向单一方向,因此更容易确定对象更新的来源和原因。
对象的展开操作有便于以不可变的方式修改对象。假设有一个描述书籍版本的对象:
const book = {
name: "JavaScript: The Definitive Guide",
author: "David Flanagan",
edition: 5,
year: 2008
};
然后出现了新的第6版。对象展开操作可以不可变的方式编写这个场景:
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
对象保持不变,不可变性得到满足。
3.6 合并对象
使用展开运算合并对象很简单,如下:
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 }
car
对象由合并三个对象创建: part1
、 part2
和 part3
。
来改变前面的例子。现在 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
”。
3.7 使用默认值填充对象
对象可以在运行时具有不同的属性集。可能设置了一些属性,也可能丢失了其他属性。
这种情况可能发生在配置对象的情况下。用户只指定需要属性,但未需要的属性取自默认值。
实现一个 multiline(str,config)
函数,该函数将 str
在给定的宽度上分成多行。
config
对象接受以下可选参数:
width:达到换行字符数, 默认为
10
newLine:要在换行处添加的字符串,默认为
\n
indent: 用来表示行的字符串,默认为空字符串
""
示例如下:
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;
}
对象展开 ...defaultConfig
从默认值中提取属性。然后 ...config
使用自定义属性值覆盖以前的默认值。
因此, safeConfig
具有 multiline()
函数所需要所有的属性。无论 multiline
有没有传入参数,都可以确保 safeConfig
具有必要的值。
3.8 深入嵌套属性
对象展开操作的最酷之处在于可以在嵌套对象上使用。在更新嵌套对象时,展开操作具有很好的可读性。
有如下一个 box
对象
const box = {
color: "red",
size: {
width: 200,
height: 100
},
items: ["pencil", "notebook"]
};
box.size
描述了 box
的大小, box.items
枚举了中 box
包含的项。
const biggerBox = {
...box,
size: {
...box.size,
height: 200
}
};
console.log(biggerBox);
/*
{
color: "red",
size: {
width: 200,
height: 200 <----- Updated value
},
items: ["pencil", "notebook"]
}
*/
...box
确保 greaterBox
从 box
接收属性。
更新嵌套对象的高度 box.size
需要一个额外的对象字面量 {...box.size,height:200}
。此对象将 box.size
的属性展开到新对象,并将高度更新为 200
。
如果将 color
更改为 black
,将 width
增加到 400
并添加新的 ruler
属性,使用展开运算就很好操作:
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
}
*/
3.9 展开 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
中提取属性。也是,没有理由在基本类型值上使用对象展开运算。
4.对象剩余操作运算
在使用解构赋值将对象的属性提取到变量之后,可以将剩余属性收集到 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
将解构其余属性 marginLeft
和 marginRight
收集到 margin
。
对象剩余(rest)操作只收集自有的和可枚举的属性。