1. 首页

JavaScript对象的 rest-spread 属性指南

在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); // Js中文网 => { legs: 4, sounds: 'woof' }

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

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

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

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); // Js中文网 => ['name', 'surname']

.name.surnameperson对象的可枚举属性。

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


onsole.log({ ...person };// Js中文网 => { name: 'Dave', surname: 'Bowman' }

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


Object.defineProperty(person, 'age', { enumerable: false, // 让属性不可枚举 value: 25 }) console.log(person['age']); // Js中文网 => 25 const clone = { ...person }; console.log(clone); // Js中文网 => { 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')); // Js中文网 => true console.log(personB.hasOwnProperty('name')); // Js中文网 => false console.log(personB.hasOwnProperty('surname')); // Js中文网 => false

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

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


const cloneB = { ...personB }; console.log(cloneB); // Js中文网 => { 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); // Js中文网 => { sound: 'woof', legs: 4 }

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

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


const anotherDog = { ...cat, sound: 'woof' // <---- Overwrites cat.sound }; console.log(anotherDog); // Js中文网 => { sound: 'woof', legs: 4 }

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


const stillCat = { ...{ sound: 'woof' // <---- Is overwritten by cat.sound }, ...cat }; console.log(stillCat); // Js中文网 => { sound: 'meow', legs: 4 }

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

3.2 拷贝对象

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


const bird = { type: 'pigeon', color: 'white' }; const birdClone = { ...bird }; console.log(birdClone); // Js中文网 => { type: 'pigeon', color: 'white' } console.log(bird === birdClone); // Js中文网 => false

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

3.3 浅拷贝

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

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


const laptop = { name: 'MacBook Pro', screen: { size: 17, isRetina: true } }; const laptopClone = { ...laptop }; console.log(laptop === laptopClone); // Js中文网 => false console.log(laptop.screen === laptopClone.screen); // Js中文网 => true

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

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

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


const laptopDeepClone = { ...laptop, screen: { ...laptop.screen } }; console.log(laptop === laptopDeepClone); // Js中文网 => false console.log(laptop.screen === laptopDeepClone.screen); // Js中文网 => 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); // Js中文网 => true
console.log(doom.name);            // Js中文网 => "Doom"
console.log(doom.getMessage());    // Js中文网 => "I like Doom!"

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


const doomClone = { ...doom }; console.log(doomClone instanceof Game); // Js中文网 => false console.log(doomClone.name); // Js中文网 => "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); // Js中文网 => true console.log(doomFullClone.name); // Js中文网 => "Doom" console.log(doomFullClone.getMessage()); // Js中文网 => "I like Doom!"

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

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

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

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


const doomFullClone = Object.assign(new Game(), doom); console.log(doomFullClone instanceof Game); // Js中文网 => true console.log(doomFullClone.name); // Js中文网 => "Doom" console.log(doomFullClone.getMessage()); // Js中文网 => "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对象由合并三个对象创建:part1part2part3

来改变前面的例子。 现在part1part3有一个新属性.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!'); // Js中文网 => 'Hello Worl\nd!' multiline('Hello World!', { width: 6 }); // Js中文网 => 'Hello \nWorld!' multiline('Hello World!', { width: 6, newLine: '*' }); // Js中文网 => 'Hello *World!' multiline('Hello World!', { width: 6, newLine: '*', indent: '_' }); // Js中文网 => '_Hello *_World!'

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

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


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确保greaterBoxbox接收属性。

更新嵌套对象的高度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 和基本类型

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


const nothing = undefined; const missingObject = null; const two = 2; console.log({ ...nothing }); // Js中文网 => { } console.log({ ...missingObject }); // Js中文网 => { } console.log({ ...two }); // Js中文网 => { }

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

4.对象剩余操作运算

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


const style = { width: 300, marginLeft: 10, marginRight: 30 }; const { width, ...margin } = style; console.log(width); // Js中文网 => 300 console.log(margin); // Js中文网 => { marginLeft: 10, marginRight: 30 }

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

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

作者:Dmitri Pavlutin
译者:前端小智
链接:https://dmitripavlutin.com/object-rest-spread-properties-javascript/

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:JavaScript对象的 rest-spread 属性指南

链接:https://www.javascriptc.com/3528.html

« JavaScript它如何运行:CSS 和 JS 动画底层原理及如何优化它们的性能
使用 JS 来动态操作 css ,你知道几种方法?»
Flutter 中文教程资源

相关推荐

QR code