三个点如何改变JavaScript对象的rest和spread属性
在JavaScript中合并多个对象是一个很常见的事情。但在JavaScript中,到目前为止并没有一种很方便的语法来进行合并。
在ES5中,通过使用Lodash的_.extend(target, [sources])
(或者其他选项),在ES2015中引入了Object.assign(target, [sources])
。
幸运的是,对象的spread
语法(ECMAScript第3阶段建议)是如何操作对象,提供了一个简短且易于遵循的语法。
const cat = {
legs: 4,
sound: 'meow'
};
const dog = {
...cat,
sound: 'woof'
};
console.log(dog);
在上面的例子中,...cat
将cat
对象复制到一个新的dog
对象中。.sound
属性的值woof
放在最后面。
这篇文章我们来了解对象的rest
和spread
语法。来看如何实现对象的克隆、合并以及属性重写等。
下面对可枚举属性简短的概括,以及如何区分自己与继承属性。这些是理解对象如何spread
和rest
语法的必要基础。
枚举和自己属性
在JavaScript中,对象是键(key
)和值(value
)之间的关联。
key
的类型通常是string
或symbol
。值可以是一个基本类型(string
、boolean
、number
、undefined
或null
),也可以是一个object
或function
。
下面的对象使用了对象字面符:
const person = {
name: 'Dave',
surname: 'Bowman'
}
person
对象描述了一个人的name
和surname
。
枚举属性
属性(property
)有几个属性(attribute
)来描述值,还有可写的、可枚举的和可配置的状态。有关于这方面更多的细节,请参阅JavaScript中的对象属性。
可枚举属性是一个布尔值,它指的是在枚举对象属性是否可访问。
可以使用Object.keys()
来枚举对象属性(有于访问自己的和可枚举的属性)。也可以使用for...in
语句(枚举所有可枚举属性),等等。
对象字面值声明的属性{prop1: 'val1', prop2:'val2'}
是可枚举的。让我们看看person
对象包含的可枚举的属性:
const keys = Object.keys(person);
console.log(keys);
name
和surname
是person
对象可枚举的属性。
有趣的部分来了。Object spread从源可枚举属性中克隆过来:
console.log({ ...person })
现在我们给person
对象创建一个不可枚举的属性age
。然后再看看Object
的spread
行为:
Object.defineProperty(person, 'age', {
enumerable: false, // 属性不可枚举
value: 25
});
console.log(person['age']); // => 25
const clone = {
...person
};
console.log(clone);
name
和surname
可枚举属从源对象person
复制到clone
对象中,但是不可枚举属性age
并没有被复制进去。
自己的属性
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
属性。
Object spread从自己的源属性中进行复制的时候,会忽略继承的属性。
const cloneB = {
...personB
};
console.log(cloneB); // => { profession: 'Astronaut' }
...personB
对象从源对象personB
中仅复制了自己的属性profession
,但忽略了可继承的name
和surname
属性。
Object spread可以从源对象中复制自己的和可枚举的属性。和
Object.keys()
相同。
传播对象属性
对象字符符中里的Object spread可以复制源对象自己的和可枚举的属性,并将其复制到目标对象中。
const targetObject = {
...sourceObject,
property: 'Value'
};
顺便说一下,在许多方面,Object spread相当于Object.assign()
。上面的代码也可以这样实现:
const targetObject = Object.assign(
{ },
sourceObject,
{ property: 'Value' }
);
一个对象字面符可以有多个Object spread,任何组合都可以使用规则属性声明:
const targetObject = {
...sourceObject1,
property1: 'Value 1',
...sourceObject2,
...sourceObject3,
property2: 'Value 2'
};
Object spread规则:最后属性获胜
当多个对象被传播,有一些属性具有相同的键时,那么是如何计算最终的值呢?规则很简单:后者扩展属性覆盖具有相同键的早期属性。
来看几个示例。下面的对象字面符实例化了一只猫:
const cat = {
sound: 'meow',
legs: 4
}
让我们扮演Frankenstein博士,把这只猫变成一只狗。注意它的sound
属性值:
const dog = {
...cat,
...{
sound: 'woof' // <----- 覆盖 cat.sound
}
};
console.log(dog); // => { sound: 'woof', legs: 4 }
后面的属性值woof
覆盖了前面的属性值meow
(来自cat
对象的sound
的值)。这符合使用相同的键值时,后一个属性值将覆盖最早的属性值的规则。
同样的规则也适用于对象初始化的规则属性:
const anotherDog = {
...cat,
sound: 'woof' // <---- Overwrites cat.sound
};
console.log(anotherDog); // => { sound: 'woof', legs: 4 }
sound: 'woof'
规则最终获胜,那是因为他在最后。
现在如果你交换传播对象的相对位置,结果是不同的:
const stillCat = {
...{
sound: 'woof' // <---- Is overwritten by cat.sound
},
...cat
};
console.log(stillCat); // => { sound: 'meow', legs: 4 }
猫仍然是猫。尽管第一个源对象提供了sound
属性的值为woof
,但它还是被后面的cat
对象的sound
的属性值meow
覆盖了。
Object spread的位置和正则性质很重要。这种语法允许实现诸如对象克隆、合并和填充默认值之类的。
下面我们来看看。
对象克隆
使用Object Spread语法可以用一个简短而富有表现力的方式来克隆一个对象。下面的例子克隆了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
的克隆。
虽然克隆对象技术乍一看似乎很简单,但有一些细节的差异还是需要注意的。
浅拷贝
Object Spread只会做一个对象的浅拷贝。只有对象本身是克隆的,而嵌套的实例不是克隆的。
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
一个额外的...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
,这是可以预期的。Object Spread不保存源对象的原型。
因此,调用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!"
我保证,使用这种方法,原型也会克隆过来。
更新不可变对象
当一个对象在应用程序的多个地方共用时,直接修改这个对象可能会带来意想不到的副作用。而且跟踪这些修改也是极为蛋疼的事情。
更好的方法是使用操作不可变。不可变能更好的控制对象的修改,有利于编写纯函数。即使在一些复杂的场景中,也更容易确定对象更新的源和原因,因为数据流到一个单一的方向。
Object Spread方便以不可变的方式来修改对象。所设你有一个对象描述了一本书的版本信息:
const book = {
name: 'JavaScript: The Definitive Guide',
author: 'David Flanagan',
edition: 5,
year: 2008
};
然后这本书的第六版本出来了。Object Spread让我们可以以不可变的方式对这个场景进行编程:
const newerBook = {
...book,
edition: 6, // <----- Overwrites book.edition
year: 2011 // <----- Overwrites book.year
};
console.log(newerBook);
...book
复制了book
对象的属性。然后手动添加edition:6
和year:2011
来更新属性值。
在最后指定重要的属性值,因为相同的键值,后面的会覆盖前面的。
newBook
是一个具有更新属性的新对象。与此同时,原来的book
对象仍然完好无损。达到我们的不变性需求。
合并对象
合并对象很简单,因为你可以扩展对象任意数量的属性。
让我们来合并三个对象:
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);
首先...part1
设置了configuration
的值为sedan
,但是后面的...part3
的configuration
设置的hatchback
覆盖了前面的。
使用默认值填充对象
对象可以在运行时拥有不同的属性集。一些属性可以被设置,另一些可能会丢失。
这种情况可能发生在配置对象的情况下。用户只能指定配置的重要属性,但未能指从默认中提取的属性。
让我们实现一个multiline(str, config)
函数,通过给定的宽度,将 str
分成多个行。
config
对象可能会接受下面几个参数:
width
:要断开的字符数。默认为10
newLine
:在行尾添加字符串。默认为\n
indent
:打算的行。默认值为' '
multiline()
函数几个示例:
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
个属性,甚至没有属性。
使用Object Spread会非常简单,可以用默认值填充config
对象。在对象字面符中首先展开默认对象,然后是config
对象:
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()
函数可以使用的全部属性。无论输入的配置是否会遗漏一些属性,safeConfig
都会具备必要的值。
Object Spread能非常直观的使用默认值。
我们需要更进一步
Object Spread非常酷的地方在于可以在嵌套对象上使用。当更新一个大对象时,这是一个很好的优势,具有很好的可读性。但还是推荐使用Object.assign()
来替代。
下面的box
对象定义了box
的标签:
const box = {
color: 'red',
size: {
width: 200,
height: 100
},
items: ['pencil', 'notebook']
};
box.size
描述了box
的尺寸,以及box.items
中包含了box
中可枚举的item
。
通过增加box.size.height
使box
变高。只需要在嵌套对象上扩展height
属性:
const biggerBox = {
...box,
size: {
...box.size,
height: 200
}
};
console.log(biggerBox);
...box
可以确保biggerBox
接收来自box
源的属性。
更新嵌套对象box.size
的height
,只需要额外的一个对象字面量{...box.size, height:200}
。这样一来,box.size
的height
属性就得到了一个新值,其值更新为200
。
我喜欢通过一个语句执行多个更新的可能性。
如何将颜色改为黑色,将宽度增加到400
,并添加一个新的项目ruler
到items
中(使用扩展数组)?这很简单:
const blackBox = {
...box,
color: 'black',
size: {
...box.size,
width: 400
},
items: [
...box.items,
'ruler'
]
};
console.log(blackBox);
传播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
中,Object Spread没有扩展任何属性。当然,没有理由在原始值上使用Object Spread。
Object rest属性
使用结构赋值将对象的属性提取到变量之后,剩余的属性可以被收集到rest对象中。
这就是对象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
属性。
Object rest只收集自己的和可枚举的属性。
注意,Object rest必须是结构性赋值中的最后一个元素。因此const { ...margin , width } = style
将会报错:SyntaxError: Rest element must be last element
。
总结
Object spread有一些规则要记住:
- 它从源对象中提取自己的和可枚举的属性
- 扩展的属性具有相同键的,后者会覆盖前者
与此同时,Object spread是简短而且富有表现力的,同时在嵌套对象上也能很好的工作,同时也保持更新的不变性。它可以轻松的实现对象克隆、合并和填充默认属性。
在结构性赋值中使用Object rest语法,可以收集剩余的属性。
实际上,Object rest和Object spread是JavaScript的重要补充。
本文根据@Dmitri Pavlutin的《How three dots changed JavaScript: object rest/spread properties》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:https://dmitripavlutin.com/how-three-dots-changed-javascript-object-rest-spread-properties/。
如需转载,烦请注明出处:https://www.fedev.cn/javascript/how-three-dots-changed-javascript-object-rest-spread-properties.htmljordan retro 11 mens purple