作者:Dmitri Pavlutin
譯者:前端小智
來源:dmitripavlutin
爲了保證的可讀性,本文采用意譯而非直譯。
在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); // => { legs: 4, sounds: 'woof' }
在上面的示例中, ...cat
將 cat
的屬性複製到新的對象 dog
中, .sound
屬性接收最終值 'woof'
。
本文將介紹對象 spread
和 rest
語法,包括對象傳播如何實現對象克隆、合併、屬性覆蓋等方法。
下面簡要介紹一下可枚舉屬性,以及如何區分自有屬性和繼承屬性。這些是理解對象 spread
和 rest
工作原理的必要基礎。
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); // => ['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
被忽略了。
2.2 自有屬性
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
被忽略。
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); // => { 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 }
對象展開中,屬性的相對位置很重要。展開語法可以實現諸如對象克隆,合併對象,填充默認值等等。
3.2 拷貝對象
使用展開語法可以很方便的拷貝對象,來創建 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
的克隆。
3.3 淺拷貝
對象展開執行的是對象的淺拷貝。僅克隆對象本身,而不克隆嵌套對象。
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
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); // => 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!"
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
對象由合併三個對象創建: 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
”。
3.7 使用默認值填充對象
對象可以在運行時具有不同的屬性集。可能設置了一些屬性,也可能丟失了其他屬性。
這種情況可能發生在配置對象的情況下。用戶只指定需要屬性,但未需要的屬性取自默認值。
實現一個 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
具有必要的值。
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
確保 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
}
*/
3.9 展開 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
中提取屬性。也是,沒有理由在基本類型值上使用對象展開運算。
4.對象剩餘操作運算
在使用解構賦值將對象的屬性提取到變量之後,可以將剩餘屬性收集到 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。
原文:https://dmitripavlutin.com/object-rest-spread-properties-javascript/