An easy guide to object rest/spread

flynike發表於2021-09-09

變一個魔術,將這隻貓變成一隻狗,注意 .sound 屬性值如何變化。

const dog = {  
  ...cat,  ...{
    sound: 'woof' // `` { sound: 'woof', legs: 4 }

後面宣告的 ·woof· 屬性值覆蓋了前面的在 cat 物件宣告的屬性值 'meow' , 符合之前所說的規則: 對於同名屬性,後宣告的值覆蓋先宣告的值。

這個規則同樣適用於物件的初始化

const anotherDog = {  
  ...cat,
  sound: 'woof' // `` { sound: 'woof', legs: 4 }

上面程式碼裡,sound: 'woof' 同樣覆蓋了之前宣告的 ' meow' 值。

現在,交換一下擴充套件物件的位置,輸出了不同的結果。

const stillCat = {  
  ...{
    sound: 'woof' // `` { sound: 'meow', legs: 4 }

cat 物件仍然是 cat 物件。雖然第一個源物件內的 .sound 屬性值是 'woof' ,但是被之後 cat 物件的 'meow' 覆蓋。

普通屬性和物件擴充套件的相對位置非常重要,這將直接影響到物件克隆,物件合併,以及填充預設屬性的結果。

下面分別詳細介紹。

2.2 克隆物件

用物件擴充套件符克隆一個物件非常簡潔,下面的程式碼克隆了一個 bird 物件。

const bird = {  
  type: 'pigeon',
  color: 'white'};

const birdClone = {  
  ...bird
};

console.log(birdClone); // => { type: 'pigeon', color: 'white' }  
console.log(bird === birdClone); // => false

...birdbird 物件的自有和可列舉屬性複製到目標物件 birdClone 內。

雖然克隆看起來很簡單,但仍然要注意其中的幾個細微之處。

淺複製

物件擴充套件只是對物件進行了 淺複製, 只有物件自身被複制,而巢狀的物件結構 沒有被複制

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 ,說明 laptopClone.screen 沒有被複制,而是 laptop.screenlaptopClone.screen 引用了同一個巢狀物件。

好的一點是,你可以在物件的任何一層使用物件擴充套件符,只需要再多做一點工作就同樣可以克隆一個巢狀物件。

const laptopDeepClone = {  
  ...laptop,  screen: {
     ...laptop.screen
  }
};

console.log(laptop === laptopDeepClone);               // => false  console.log(laptop.screen === laptopDeepClone.screen); // => false

使用 ...laptop.screen 使巢狀物件也被克隆,現在 laptopDeepClone 完全克隆了 laptop

原型失去了

下面的程式碼宣告瞭一個 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 現在只是一個普通的 JavaScript 物件,它的原型是 Object.prototype 而不是預想中的Game.prototype物件擴充套件不保留源物件的原型。

因此呼叫 doomClone.getMessage() 方法會丟擲一個 TypeError 錯誤,因此 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!"

這樣,就保留了原型。

2.3 不可變物件更新

在一個應用裡,同一個物件可能會用於多個地方,直接修改這個物件會帶來意想不到的副作用,並且追蹤這個修改及其困難。

一個好的方式是使操作不可變。不可變性使修改物件更為可控,更有利於書寫。。即時是在複雜的應用場景,由於單向資料流,更容易確定物件的來源和改變的原因。

使用物件擴充套件能更方便的以不可變方式來修改一個物件。假設現在你有一個物件來描述一本書的資訊。

const book = {  
  name: 'JavaScript: The Definitive Guide',
  author: 'David Flanagan',
  edition: 5,
  year: 2008
};

現在,書第六版即將出版,我們用物件擴充套件的處理這個場景。

const newerBook = {  
  ...book,
  edition: 6,  // 

newerBook 物件內的 ...book 擴充套件了 book 物件的屬性。手動建立的可列舉屬性 editon: 6year: 2011 更新了原有的同名屬性。

重要的屬性一般在末尾來指定,以便覆蓋前面已經建立的同名屬性。

newerBook 是一個更新了某些屬性的新的物件,並且我們沒有改變原有的 book 物件,滿足了不可變性的要求。

2.4 合併物件

使用物件擴充套件符合並多個物件非常簡單。

現在我們合併3個物件來建立一個“合成物件”。

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 }

上面的例子中,我們使用 part1part2part3 3個物件合併成了一個 car 物件。

另外,不要忘了之前講的規則,後面的屬性值會覆蓋前面的同名屬性值。這是我們合併有同名屬性物件的計算依據。

現在我們稍微改變一下之前的程式碼。給 part1part3 增加一個 .configuration 屬性。

const part1 = {  
  color: 'white',
  configuration: 'sedan'};const part2 = {  
  model: 'Honda'};const part3 = {  
  year: 2005,
  configuration: 'hatchback'};const car = {  
  ...part1,
  ...part2,
  ...part3 // 

...part1configuration 屬性設定成了 'sedan'。然而之後的擴充套件符 ...part3 覆蓋了之前的同名 .configuration,最終生成的物件值為 'hatchback'

2.5 給物件設定預設值

一個物件在程式執行時可能會有多套不同的屬性值,有些屬性可能會被設定,有些則可能被忽略。

這種情況通常發生在一個配置物件上。使用者可以指定一個重要的屬性值,不重要的屬性則使用預設值。

現在我們來實現一個 multline(str, config) 方法,將str 按照給定的長度分割成多行。

config 物件接受下面3個可選的引數。

  • width: 分割的字元長度,預設是 10

  • newLine: 新增到每一行結尾的的字元, 預設是 n

  • indent: 每一行開頭的縮排符,預設是空字串 ''

下面是一些 multline() 執行的例子。

multiline('Hello World!');  
// =>` 'Hello Worlnd!'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;
}

我們來仔細瞭解一下 safeConfig 物件。

...defaultConfig 首先將預設物件的屬性複製,隨後,...config 裡使用者自定義的值覆蓋了之前的預設屬性值。

這樣 safeConfig 值就擁有了所有 multiline() 需要的配置引數。無論呼叫 multiline() 函式時,輸入的 config 是否缺失了某些屬性,都可以保證 safeConfig 擁有所有的必備引數。

顯而易見,物件擴充套件實現了我們想要的 給物件設定預設值。

2.6 更加深入

物件擴充套件更有用的一點是用於巢狀物件,當更新一個複雜物件時,更具有可讀性,比 Object.assign() 更值得推薦。

下面的 box 物件定義一個盒子及盒子內的物品。

const box = {  
  color: 'red',  size: {    width: 200, 
    height: 100 
  },
  items: ['pencil', 'notebook']
};

box.size 描述了這個盒子的尺寸,box.items 列舉了盒子內的物品。

為了使盒子看起來更高,我們增大 box.size.height 的值,只需要在巢狀物件上使用 物件擴充套件符

const biggerBox = {  
  ...box,  size: {
    ...box.size,    height: 200
  }
};
console.log(biggerBox);  
/*
{
  color: 'red',
  size: {
    width: 200, 
    height: 200 

...box 確保了 biggerBox 獲得了 源物件 box 上的全部屬性。

更新 box.size 的 height 值需要額外一個 {...box.size, height: 200} 物件,該物件接收 box.size 的全部屬性,並將 height 值更新至 200

只需要一個語句就能更新物件的多處屬性。

現在如果我們還想把顏色改成 black ,增加盒子的寬度到 400, 並且再放一把尺子到盒子內,應該怎麼辦?同樣很簡單。

const blackBox = {  
  ...box,  color: 'black',  size: {
    ...box.size,    width: 400
  },
  items: [
    ...box.items,    'ruler'
  ]
};
console.log(blackBox);  
/*
{
  color: 'black', 

2.7 擴充套件 undefinednull原始型別值

如果在 undefinednull原始型別值 上使用原始型別的值,不會複製任何屬性,也不會丟擲錯誤,只是簡單的返回一個空物件。

const nothing = undefined;  
const missingObject = null;  
const two = 2;console.log({ ...nothing });       // => { }  console.log({ ...missingObject }); // => { }  console.log({ ...two });           // => { }

如上所示:從 nothing, missingObjecttwo不會複製任何屬性。

當然,這只是一個演示,畢竟根本沒有理由在一個原始型別的值上面使用物件擴充套件符。

3. 剩餘屬性

當使用解構賦值將物件的屬性值賦值給變數後,剩餘的屬性值將會被集合進一個剩餘物件內。

下面的程式碼演示了怎麼使用 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 則獲得了 style 物件的其餘屬性,margin 物件獲取了 marginLeftmarginRight 屬性。

rest 運算子同樣只會獲取自有屬性和可列舉屬性。

注意,在解構賦值內,rest 運算子只能放到最後,因此 const { ...margin , width } = style 無效,並會丟擲一個 SyntaxError: Rest element must be last element 錯誤。

4. 結論

物件擴充套件需要以下幾點:

  • 它只會提取物件的自有屬性和可列舉屬性

  • 後定義的屬性值會覆蓋之前定義過的同名屬性值

同時,物件擴充套件使用上方便簡潔,能更好的處理巢狀物件,保持不可變性,在實現物件克隆和填充預設屬性值上也使用方便。

rest 運算子在解構賦值時可以收集剩餘的屬性。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2618/viewspace-2800791/,如需轉載,請註明出處,否則將追究法律責任。

相關文章