JS對象的 rest/spread 屬性指南

做者: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' }
複製代碼

在上面的示例中,...catcat的屬性複製到新的對象dog中,.sound屬性接收最終值'woof'微信

本文將介紹對象spreadrest語法,包括對象傳播如何實現對象克隆、合併、屬性覆蓋等方法。數據結構

下面簡要介紹一下可枚舉屬性,以及如何區分自有屬性和繼承屬性。這些是理解對象spreadrest工做原理的必要基礎。ide

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.surnameperson對象的可枚舉屬性。

接下來是有趣的部分, 對象展開來自源可枚舉屬性的副本:

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對中。所以,birdClonebird的克隆。

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.screenlaptopClone.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對象由合併三個對象建立: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!');
// => '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,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 });       // => { }
console.log({ ...missingObject }); // => { }
console.log({ ...two });           // => { }
複製代碼

對象展開操做沒有從nothingmissingObjecttwo中提取屬性。也是,沒有理由在基本類型值上使用對象展開運算。

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將解構其他屬性marginLeftmarginRight收集到margin

對象剩餘(rest)操做只收集自有的和可枚舉的屬性。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:dmitripavlutin.com/object-rest…

交流

阿(a)裏(li)雲(yun)最近有活動低至1折,有興趣能夠看看: promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索