做者:Dmitri Pavlutinjavascript
译者:前端小智html
来源:dmitripavlutin前端
为了保证的可读性,本文采用意译而非直译。java
在ES5中,我们合并对象一般使用Lodash的_.extend(target, [sources])
方法,在ES6中我们使用 Object.assign(target, [sources])
来合并对象,固然如今最经常使用应该是使用 Rest/Spread
(展开运算符与剩余操做符)。git
来个例子:github
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
工做原理的必要基础。ide
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
。
JS中的对象是键和值之间的关联。键
类型一般是字符串或symbol
。值
能够是基本类型(string、boolean、number、undefined或null)、对象或函数。
下面使用对象字面量来建立对象:
const person = {
name: 'Dave',
surname: 'Bowman'
};
复制代码
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
被忽略了。
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
被忽略。
对象展开语法从源对象中提取自有和可枚举的属性,并将它们复制到目标对象中。
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'
};
复制代码
当多个对象展开而且某些属性具备相同的键时,最终值是如何计算的? 规则很简单:后展开属性会覆盖前端相同属性。
来看看几个盒子,下面有一个对象 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 }
复制代码
对象展开中,属性的相对位置很重要。 展开语法能够实现诸如对象克隆,合并对象,填充默认值等等。
使用展开语法能够很方便的拷贝对象,来建立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
的克隆。
对象展开执行的是对象的浅拷贝。 仅克隆对象自己,而不克隆嵌套对象。
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
复制代码
下面的代码片断声明了一个类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!"
复制代码
当在应用程序的许多位置共享同一对象时,对其进行直接修改可能会致使意外的反作用。 追踪这些修改是一项繁琐的工做。
更好的方法是使操做不可变。 不变性保持在更好的控制对象的修改和有利于编写纯函数。 即便在复杂的场景中,因为数据流向单一方向,所以更容易肯定对象更新的来源和缘由。
对象的展开操做有便于以不可变的方式修改对象。 假设咋样有一个描述书籍版本的对象:
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
对象保持不变,不可变性获得知足。
使用展开运算合并对象很简单,以下:
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
”。
对象能够在运行时具备不一样的属性集。可能设置了一些属性,也可能丢失了其余属性。
这种状况可能发生在配置对象的状况下。用户只指定须要属性,但未须要的属性取自默认值。
实现一个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
具备必要的值。
对象展开操做的最酷之处在于能够在嵌套对象上使用。在更新嵌套对象时,展开操做具备很好的可读性。
有以下一个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
}
*/
复制代码
当展开的属性为undefined
、null
或基本数据类型时,不会提取属性,也不会抛出错误,返回结果只是一个纯空对象:
const nothing = undefined;
const missingObject = null;
const two = 2;
console.log({ ...nothing }); // => { }
console.log({ ...missingObject }); // => { }
console.log({ ...two }); // => { }
复制代码
对象展开操做没有从nothing
、missingObject
和two
中提取属性。也是,没有理由在基本类型值上使用对象展开运算。
在使用解构赋值将对象的属性提取到变量以后,能够将剩余属性收集到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)操做只收集自有的和可枚举的属性。
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug。
原文:dmitripavlutin.com/object-rest…
阿(a)里(li)云(yun)最近有活动低至1折,有兴趣能够看看: promotion.aliyun.com/ntms/yunpar…
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,通常都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励