JS對象的 rest/spread 屬性指南

作者: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參數接受不同的屬性集:可以給定 12或 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.sizeheight200}。此對象將 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/

交流

640?wx_fmt=jpeg

延伸閱讀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章