当前位置: 代码迷 >> 综合 >> JS对象的 rest/spread 属性指南。
  详细解决方案

JS对象的 rest/spread 属性指南。

热度:97   发布时间:2024-01-29 19:54:48.0

转载自品略图书馆  http://www.pinlue.com/article/2019/10/2516/299738779449.html

 

为了保证的可读性,本文采用意译而非直译。

在ES5中,咱们合并对象通常使用Lodash的 _.extend(target,[sources]) 方法,在ES6中咱们使用 Object.assign(target,[sources])来合并对象,当然现在最常用应该是使用 Rest/Spread(展开运算符与剩余操作符)。

来个例子:

 
  1. const cat = {

  2. legs: 4,

  3. sound: "meow"

  4. };

  5. const dog = {

  6. ...cat,

  7. sound: "woof"

  8. };

  9.  

  10. console.log(dog); // => { legs: 4, sounds: "woof" }

在上面的示例中, ...cat将 cat的属性复制到新的对象 dog中, .sound属性接收最终值 "woof"

本文将介绍对象 spread 和 rest语法,包括对象传播如何实现对象克隆、合并、属性覆盖等方法。

下面简要介绍一下可枚举属性,以及如何区分自有属性和继承属性。这些是理解对象 spread 和 rest工作原理的必要基础。

1.属性描述对象

JS 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。

下面是属性描述对象的一个例子。

 
  1. {

  2.  

  3. value: 123,

  4.  

  5. writable: false,

  6.  

  7. enumerable: true,

  8.  

  9. configurable: false,

  10.  

  11. get: undefined,

  12.  

  13. set: undefined

  14. }

属性描述对象提供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)、对象或函数。

下面使用对象字面量来创建对象:

 
  1. const person = {

  2. name: "Dave",

  3. surname: "Bowman"

  4. };

2.1 可枚举的属性

enumerable 属性是一个布尔值,它表示在枚举对象的属性时该属性是否可访问。

咱们可以使用 object.keys()(访问自有和可枚举的属性)枚举对象属性,例如,在 for..in语句中(访问所有可枚举属性)等等。

在对象字面量 {prop1:"val1",prop2:"val2"}中显式声明的属性是可枚举的。来看看 person对象包含哪些可枚举属性:

 
  1. const keys = Object.keys(person);

  2. console.log(keys); // => ["name", "surname"]

.name和 .surname是 person对象的可枚举属性。

接下来是有趣的部分, 对象展开来自源可枚举属性的副本:

 
  1. onsole.log({ ...person };// => { name: "Dave", surname: "Bowman" }

现在,在 person对象上创建一个不可枚举的属性 .age。然后看看展开的行为:

 
  1. Object.defineProperty(person, "age", {

  2. enumerable: false, // 让属性不可枚举

  3. value: 25

  4. })

  5. console.log(person["age"]); // => 25

  6.  

  7. const clone = {

  8. ...person

  9. };

  10. console.log(clone); // => { name: "Dave", surname: "Bowman" }

.name和 .surname可枚举属性从源对象 person复制到 clone,但是不可枚举的 .age被忽略了。

2.2 自有属性

JS包含原型继承。因此,对象属性既可以是自有的,也可以是继承的

在对象字面量显式声明的属性是自有的。但是对象从其原型接收的属性是继承的。

接着创建一个对象 personB并将其原型设置为 person

 
  1. const personB = Object.create(person, {

  2. profession: {

  3. value: "Astronaut",

  4. enumerable: true

  5. }

  6. });

  7.  

  8. console.log(personB.hasOwnProperty("profession")); // => true

  9. console.log(personB.hasOwnProperty("name")); // => false

  10. console.log(personB.hasOwnProperty("surname")); // => false

personB对象具有自己的属性 .professional,并从原型 person继承 .name和 .surname属性。

展开运算只展开自有属性,忽略继承属性。

 
  1. const cloneB = {

  2. ...personB

  3. };

  4. console.log(cloneB); // => { profession: "Astronaut" }

对象展开 ...personB 只从源对象 personB复制,继承的 .name和 .surname被忽略。

3. 对象展开属性

对象展开语法从源对象中提取自有和可枚举的属性,并将它们复制到目标对象中。

 
  1. const targetObject = {

  2. ...sourceObject,

  3. property: "Value"

  4. };

在许多方面,对象展开语法等价于 object.assign(),上面的代码也可以这样实现

 
  1. const targetObject = Object.assign(

  2. {},

  3. sourceObject,

  4. { property: "Value"}

  5. )

对象字面量可以具有多个对象展开,与常规属性声明的任意组合:

 
  1. const targetObject = {

  2. ...sourceObject1,

  3. property1: "Value 1",

  4. ...sourceObject2,

  5. ...sourceObject3,

  6. property2: "Value 2"

  7. };

3.1 对象展开规则:后者属性会覆盖前面属性

当多个对象展开并且某些属性具有相同的键时,最终值是如何计算的?规则很简单:后展开属性会覆盖前端相同属性。

来看看几个盒子,下面有一个对象 cat

 
  1. const cat = {

  2. sound: "meow",

  3. legs: 4

  4. };

接着把这只猫变成一只狗,注意 .sound属性的值

 
  1. const dog = {

  2. ...cat,

  3. ...{

  4. sound: "woof" // <----- Overwrites cat.sound

  5. }

  6. };

  7. console.log(dog); // => { sound: "woof", legs: 4 }

后一个值“ woof”覆盖了前面的值“ meow”(来自 cat源对象)。这与后一个属性使用相同的键覆盖最早的属性的规则相匹配。

相同的规则适用于对象初始值设定项的常规属性:

 
  1. const anotherDog = {

  2. ...cat,

  3. sound: "woof" // <---- Overwrites cat.sound

  4. };

  5. console.log(anotherDog); // => { sound: "woof", legs: 4 }

现在,如果您交换展开对象的相对位置,结果会有所不同:

 
  1. const stillCat = {

  2. ...{

  3. sound: "woof" // <---- Is overwritten by cat.sound

  4. },

  5. ...cat

  6. };

  7. console.log(stillCat); // => { sound: "meow", legs: 4 }

对象展开中,属性的相对位置很重要。展开语法可以实现诸如对象克隆,合并对象,填充默认值等等。

3.2 拷贝对象

使用展开语法可以很方便的拷贝对象,来创建 bird对象的一个副本。

 
  1. const bird = {

  2. type: "pigeon",

  3. color: "white"

  4. };

  5.  

  6. const birdClone = {

  7. ...bird

  8. };

  9.  

  10. console.log(birdClone); // => { type: "pigeon", color: "white" }

  11. console.log(bird === birdClone); // => false

...bird将自己的和可枚举的 bird属性复制到 birdClone对中。因此, birdClone是 bird的克隆。

3.3 浅拷贝

对象展开执行的是对象的浅拷贝。仅克隆对象本身,而不克隆嵌套对象。

laptop一个嵌套的对象 laptop.screen。让咱们克隆 laptop,看看它如何影响嵌套对象:

 
  1. const laptop = {

  2. name: "MacBook Pro",

  3. screen: {

  4. size: 17,

  5. isRetina: true

  6. }

  7. };

  8. const laptopClone = {

  9. ...laptop

  10. };

  11.  

  12. console.log(laptop === laptopClone); // => false

  13. console.log(laptop.screen === laptopClone.screen); // => true

第一个比较 laptop===laptopClone 结果为 false,表明正确地克隆了主对象。

然而 laptop.screen===laptopClone.screen结果为 true,这意味着 laptop.screen和 laptopClone.screen引用了相同对象。

当然可以在嵌套对象使用展开属性,这样就能克隆嵌套对象。

 
  1. const laptopDeepClone = {

  2. ...laptop,

  3. screen: {

  4. ...laptop.screen

  5. }

  6. };

  7.  

  8. console.log(laptop === laptopDeepClone); // => false

  9. console.log(laptop.screen === laptopDeepClone.screen); // => false

3.4 原型丢失

下面的代码片段声明了一个类 Game,并创建了这个类 doom的实例

 
  1. class Game {

  2. constructor(name) {

  3. this.name = name;

  4. }

  5.  

  6. getMessage() {

  7. return `I like ${this.name}!`;

  8. }

  9. }

  10.  

  11. const doom = new Game("Doom");

  12. console.log(doom instanceof Game); // => true

  13. console.log(doom.name); // => "Doom"

  14. console.log(doom.getMessage()); // => "I like Doom!"

现在克隆从构造函数调用创建的 doom实例,这里会有点小意外:

 
  1. const doomClone = {

  2. ...doom

  3. };

  4.  

  5. console.log(doomClone instanceof Game); // => false

  6. console.log(doomClone.name); // => "Doom"

  7. console.log(doomClone.getMessage());

  8. // TypeError: doomClone.getMessage is not a function

...doom仅仅将自己的属性 .name复制到 doomClone中,其它都没有。

doomClone是一个普通的JS对象,原型是 Object.prototype,但不是 Game.prototype。所以对象展开不保留源对象的原型。

因此,调用 doomClone.getMessage()会抛出一个类型错误,因为 doomClone不继承 getMessage()方法。

要修复缺失的原型,需要手动指定 __proto__

 
  1. const doomFullClone = {

  2. ...doom,

  3. __proto__: Game.prototype

  4. };

  5.  

  6. console.log(doomFullClone instanceof Game); // => true

  7. console.log(doomFullClone.name); // => "Doom"

  8. console.log(doomFullClone.getMessage()); // => "I like Doom!"

对象内的 __proto__确保 doomFullClone具有必要的原型 Game.prototype

不要在项目中使用 __proto__,这种是很不推荐的。这边只是为了演示而已。

对象展开构造函数调用创建的实例,因为它不保留原型。其目的是以一种浅显的方式扩展自己的和可枚举的属性,因此忽略原型的方法似乎是合理的。

另外,还有一种更合理的方法可以使用 Object.assign()克隆 doom

 
  1. const doomFullClone = Object.assign(new Game(), doom);

  2.  

  3. console.log(doomFullClone instanceof Game); // => true

  4. console.log(doomFullClone.name); // => "Doom"

  5. console.log(doomFullClone.getMessage()); // => "I like Doom!"

3.5 不可变对象更新

当在应用程序的许多位置共享同一对象时,对其进行直接修改可能会导致意外的副作用。追踪这些修改是一项繁琐的工作。

更好的方法是使操作不可变。不变性保持在更好的控制对象的修改和有利于编写纯函数。即使在复杂的场景中,由于数据流向单一方向,因此更容易确定对象更新的来源和原因。

对象的展开操作有便于以不可变的方式修改对象。假设有一个描述书籍版本的对象:

 
  1. const book = {

  2. name: "JavaScript: The Definitive Guide",

  3. author: "David Flanagan",

  4. edition: 5,

  5. year: 2008

  6. };

然后出现了新的第6版。对象展开操作可以不可变的方式编写这个场景:

 
  1. const newerBook = {

  2. ...book,

  3. edition: 6, // <----- Overwrites book.edition

  4. year: 2011 // <----- Overwrites book.year

  5. };

  6.  

  7. console.log(newerBook);

  8. /*

  9. {

  10. name: "JavaScript: The Definitive Guide",

  11. author: "David Flanagan",

  12. edition: 6,

  13. year: 2011

  14. }

  15. */

newerBook是一个具有更新属性的新对象。与此同时,原 book对象保持不变,不可变性得到满足。

3.6 合并对象

使用展开运算合并对象很简单,如下:

 
  1. const part1 = {

  2. color: "white"

  3. };

  4. const part2 = {

  5. model: "Honda"

  6. };

  7. const part3 = {

  8. year: 2005

  9. };

  10.  

  11. const car = {

  12. ...part1,

  13. ...part2,

  14. ...part3

  15. };

  16. console.log(car); // { color: "white", model: "Honda", year: 2005 }

car对象由合并三个对象创建: part1、 part2和 part3

来改变前面的例子。现在 part1和 part3有一个新属性 .configuration

 
  1. const part1 = {

  2. color: "white",

  3. configuration: "sedan"

  4. };

  5. const part2 = {

  6. model: "Honda"

  7. };

  8. const part3 = {

  9. year: 2005,

  10. configuration: "hatchback"

  11. };

  12.  

  13. const car = {

  14. ...part1,

  15. ...part2,

  16. ...part3 // <--- part3.configuration overwrites part1.configuration

  17. };

  18. console.log(car);

  19. /*

  20. {

  21. color: "white",

  22. model: "Honda",

  23. year: 2005,

  24. configuration: "hatchback" <--- part3.configuration

  25. }

  26. */

第一个对象展开 ...part1将 .configuration的值设置为" sedan"。然而, ...part3 覆盖了之前的 .configuration值,使其最终成为“ hatchback”。

3.7 使用默认值填充对象

对象可以在运行时具有不同的属性集。可能设置了一些属性,也可能丢失了其他属性。

这种情况可能发生在配置对象的情况下。用户只指定需要属性,但未需要的属性取自默认值。

实现一个 multiline(str,config)函数,该函数将 str在给定的宽度上分成多行。

config对象接受以下可选参数:

  •  

    width:达到换行字符数, 默认为 10

  •  

    newLine:要在换行处添加的字符串,默认为 \n

  •  

    indent: 用来表示行的字符串,默认为空字符串 ""

示例如下:

 
  1. multiline("Hello World!");

  2. // => "Hello Worl\nd!"

  3.  

  4. multiline("Hello World!", { width: 6 });

  5. // => "Hello \nWorld!"

  6.  

  7. multiline("Hello World!", { width: 6, newLine: "*" });

  8. // => "Hello *World!"

  9.  

  10. multiline("Hello World!", { width: 6, newLine: "*", indent: "_" });

  11. // => "_Hello *_World!"

config参数接受不同的属性集:可以给定 12或 3个属性,甚至不指定也是可等到的。

使用对象展开操作用默认值填充配置对象相当简单。在对象字面量,首先展开缺省对象,然后是配置对象:

 
  1. function multiline(str, config = {}) {

  2. const defaultConfig = {

  3. width: 10,

  4. newLine: "\n",

  5. indent: ""

  6. };

  7. const safeConfig = {

  8. ...defaultConfig,

  9. ...config

  10. };

  11. let result = "";

  12. // Implementation of multiline() using

  13. // safeConfig.width, safeConfig.newLine, safeConfig.indent

  14. // ...

  15. return result;

  16. }

对象展开 ...defaultConfig 从默认值中提取属性。然后 ...config 使用自定义属性值覆盖以前的默认值。

因此, safeConfig具有 multiline()函数所需要所有的属性。无论 multiline有没有传入参数,都可以确保 safeConfig具有必要的值。

3.8 深入嵌套属性

对象展开操作的最酷之处在于可以在嵌套对象上使用。在更新嵌套对象时,展开操作具有很好的可读性。

有如下一个 box对象

 
  1. const box = {

  2. color: "red",

  3. size: {

  4. width: 200,

  5. height: 100

  6. },

  7. items: ["pencil", "notebook"]

  8. };

box.size描述了 box的大小, box.items枚举了中 box包含的项。

 
  1. const biggerBox = {

  2. ...box,

  3. size: {

  4. ...box.size,

  5. height: 200

  6. }

  7. };

  8. console.log(biggerBox);

  9. /*

  10. {

  11. color: "red",

  12. size: {

  13. width: 200,

  14. height: 200 <----- Updated value

  15. },

  16. items: ["pencil", "notebook"]

  17. }

  18. */

...box确保 greaterBox从 box接收属性。

更新嵌套对象的高度 box.size需要一个额外的对象字面量 {...box.size,height:200}。此对象将 box.size的属性展开到新对象,并将高度更新为 200

如果将 color更改为 black,将 width增加到 400并添加新的 ruler属性,使用展开运算就很好操作:

 
  1. const blackBox = {

  2. ...box,

  3. color: "black",

  4. size: {

  5. ...box.size,

  6. width: 400

  7. },

  8. items: [

  9. ...box.items,

  10. "ruler"

  11. ]

  12. };

  13. console.log(blackBox);

  14. /*

  15. {

  16. color: "black", <----- Updated value

  17. size: {

  18. width: 400, <----- Updated value

  19. height: 100

  20. },

  21. items: ["pencil", "notebook", "ruler"] <----- A new item ruler

  22. }

  23. */

3.9 展开 undefined,null 和基本类型

当展开的属性为 undefined、 null或基本数据类型时,不会提取属性,也不会抛出错误,返回结果只是一个纯空对象:

 
  1. const nothing = undefined;

  2. const missingObject = null;

  3. const two = 2;

  4.  

  5. console.log({ ...nothing }); // => { }

  6. console.log({ ...missingObject }); // => { }

  7. console.log({ ...two }); // => { }

对象展开操作没有从 nothing、 missingObject和 two中提取属性。也是,没有理由在基本类型值上使用对象展开运算。

4.对象剩余操作运算

在使用解构赋值将对象的属性提取到变量之后,可以将剩余属性收集到 rest对象中。

 
  1. const style = {

  2. width: 300,

  3. marginLeft: 10,

  4. marginRight: 30

  5. };

  6.  

  7. const { width, ...margin } = style;

  8.  

  9. console.log(width); // => 300

  10. console.log(margin); // => { marginLeft: 10, marginRight: 30 }

解构赋值定义了一个新的变量 width,并将其值设置为 style.width。对象剩余操作 ...margin将解构其余属性 marginLeft和 marginRight收集到 margin

对象剩余(rest)操作只收集自有的和可枚举的属性。

  相关解决方案