做者: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次左右,挺苦的,還望支持,給點鼓勵