Dans ES5, _.extend(target, [sources])
a été introduit dans ES2015 en utilisant Object.assign(target, [sources])
de Lodash (ou d'autres options).
Heureusement, la syntaxe spread
pour les objets (recommandations ECMAScript Phase 3) fournit une syntaxe courte et facile à suivre sur la façon de manipuler les objets.
const cat = { legs: 4, sound: 'meow'};const dog = { ...cat, sound: 'woof'}; console.log(dog);
Dans l'exemple ci-dessus, ...cat
copie l'objet cat
dans un nouvel objet dog
. .sound
La valeur de l'attribut woof
est placée à la fin.
Dans cet article, nous découvrirons la syntaxe rest
et spread
des objets. Voyons comment implémenter le clonage d'objets, la fusion, la réécriture d'attributs, etc.
Ce qui suit est un bref résumé des propriétés énumérables et comment faire la distinction entre les propriétés propres et héritées. Ce sont les bases nécessaires pour comprendre la syntaxe des objets spread
et rest
.
Énumérations et propriétés propres
En JavaScript, un objet est une association entre une clé (key
) et une valeur (value
). Le type de
key
est généralement string
ou symbol
. La valeur peut être un type primitif (string
, boolean
, number
, undefined
ou null
), ou un object
ou function
.
Les objets suivants utilisent des littéraux d'objet :
const person = { name: 'Dave', surname: 'Bowman'}
person
L'objet décrit le name
et le surname
d'une personne.
Propriétés énumérées
Les propriétés (property
) ont plusieurs propriétés (attribute
) pour décrire les valeurs, ainsi que les états inscriptibles, énumérables et configurables. Pour plus de détails à ce sujet, consultez Propriétés des objets en JavaScript.
La propriété énumérable est une valeur booléenne, qui indique si la propriété de l'objet énuméré est accessible.
Vous pouvez utiliser Object.keys()
pour énumérer les propriétés d'un objet (pour accéder à ses propriétés propres et énumérables). Vous pouvez également utiliser l'instruction for...in
(pour énumérer toutes les propriétés énumérables), etc.
Les propriétés déclarées par les littéraux d'objet {prop1: 'val1', prop2:'val2'}
sont énumérables. Examinons les propriétés énumérables que contient l'objet person
:
const keys = Object.keys(person); console.log(keys);
name
et surname
sont énumérables par la propriété de l'objet person
.
Voici la partie amusante. La répartition de l'objet est clonée à partir de la propriété énumérable source :
console.log({ ...person })
Maintenant, nous créons une propriété non énumérable pour l'person
objet age
. Ensuite, regardez le Object
comportement de spread
:
Object.defineProperty(person, 'age', { enumerable: false, // 属性不可枚举 value: 25}); console.log(person['age']); // => 25const clone = { ...person }; console.log(clone);
name
et surname
les propriétés énumérables sont copiées à partir de l'objet source person
clone
objet, mais l'attribut non énumérable age
n'y est pas copié.
Propriétés propres
Les prototypes JavaScript peuvent être hérités. Par conséquent, les propriétés de l'objet peuvent être les vôtres ou héritées.
Les propriétés explicitement déclarées par le littéral d'objet sont les siennes, mais les propriétés que l'objet reçoit du prototype sont héritées.
Ensuite, créez un personB
objet et définissez ses propriétés sur person
: L'objet
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
a ses propres propriétés profession
et de person
Le < Attributs 🎜> et name
hérités de l'attribut. surname
Lorsque l'objet diffuse des copies à partir de ses propres propriétés source, il ignore les propriétés héritées. L'objet
const cloneB = { ...personB }; console.log(cloneB); // => { profession: 'Astronaut' }
copie uniquement ses propres propriétés ...personB
de l'objet source personB
, mais ignore les propriétés profession
et name
héritables. surname
La répartition d'objets peut copier ses propres propriétés et celles énumérables de l'objet source. Identique àPropriétés de l'objet Spread L'objet spread dans le caractère de l'objet peut copier les propriétés propres et énumérables de l'objet source et les copier dans l'objet cible..
Object.keys()
const targetObject = { ...sourceObject, property: 'Value'};
. Le code ci-dessus peut également être implémenté comme ceci : Object.assign()
const targetObject = Object.assign( { }, sourceObject, { property: 'Value' } );
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的重要补充。
相关推荐: