---
# System prepended metadata

title: '[JavaScript] Array / Object destructuring（陣列 / 物件解構）'
tags: [前端筆記]

---

# [JavaScript] Array / Object destructuring（陣列 / 物件解構）
###### tags: `前端筆記`

此篇筆記是這禮拜等車的時候看完 Medium 文章 [My Boss: You Know ES6, But Why Don’t You Use It? 😠](https://javascript.plainenglish.io/my-boss-you-know-es6-but-why-dont-you-use-it-5e0316f14c67) 後發現文中的第一、二點（Complaints about extracting object values 以及 Complaints about merging data）發現陣列 / 物件解構竟然可以這麼方便，所以就上網找其他大神的解構範例。

## 解構的含義

> 解構賦值是用在"陣列指定陣列"或是"物件指定物件"的情況下，這個時候會根據陣列原本的結構，以類似"鏡子"對映樣式(pattern)來進行賦值。
> *[ref.: 展開運算符與其餘運算符](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/rest_spread.html)*

所以解構其實就類似於鏡子的反射，所以需要把原本的東西放在右邊，鏡子反射的東西在左邊：

```javascript=
// 鏡子反射 =（鏡子）= 原本的東西
const [x, y, z] = [1, 2, 3];
```

## 解構不會更改原本的東西的值

> It’s called “destructuring assignment,” because it “destructurizes” by copying items into variables. But the array itself is not modified.
> *[ref.: Destructuring assignment - “Destructuring” does not mean “destructive”.](https://javascript.info/destructuring-assignment)*


## 陣列解構

### 1. 依照位置給新的名字

```javascript=
const arr = [1, 2, 3];
const [first, second, third] = arr;

console.log(first); // 1
console.log(second); // 2
console.log(third); // 3
```

可以直接給變數名稱替代 `arr[index]` 取值，所以以上的範例等價於：

```javascript=
const arr = [1, 2, 3];

console.log(arr[0]); // 1
console.log(arr[1]); // 2
console.log(arr[2]); // 3
```

### 2. 按照順序跳過中途的值

```javascript=
const arr = [1, 2, 3, 4, 5];
// 中間的 3 被跳過了
const [a, b, , d] = arr;

console.log(a); // 1
console.log(b); // 2
console.log(d); // 4
```

### 3. 使用其餘運算子直接給我剩下的值

單純解構陣列就會捨棄其他沒解構出來的值：

```javascript=
const arr = [1, 2, 3, 4, 5];
const [a , b] = arr;

console.log(a); // 1
console.log(b); // 2
// 剩下的 3, 4, 5  都拿不到 
```

一組陣列常常都會有很多值，透過解構 + 其餘運算子（...）可以直接得到解構剩下的值：
> 其餘運算符是收集其餘的(剩餘的)這些值，轉變成一個陣列。
> *[ref.: 其餘運算符(Rest Operator)](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/rest_spread.html)*

```javascript=
const arr = [1, 2, 3, 4, 5];
const [a, b, ...rest] = arr;

// 其餘運算子的變數名稱可以隨便取，但記得要有 ... 三個點的其餘運算子
const names = ['Lun', 'Chun', 'Jie', 'Tim'];
const [name1, name2, ...title] = names;

console.log(name1); // Lun
console.log(name2); // Chun
console.log(title); // ['Jie', 'Tim']
```

當然也可以跳過 + 剩餘的組合：

```javascript=
const names = ['Lun', 'Chun', 'Yoyo', 'Keke'];
const [name1, , name3, ...restNames] = names;

console.log(name1); // 'Lun'
console.log(name3); // 'Yoyo'
console.log(restNames); // ['Keke']
```

### 4. 如果鏡子的反射之變數長度大於原本陣列只會有 `undefined`，不會報錯

```javascript=
const [name1, name2] = [];

console.log(name1); // undefined
console.log(name2); // undefined
```

### 5. 如果沒找到就給預設值

但如果有找到值就不用預設值，而是用找到的值：

```javascript=
const [name1 = 'Lun', name2 = 'Chun'] = [];

console.log(name1); // 'Lun'
console.log(name2); // 'Chun'

const [name3 = 'CC'] = ['ABCD'];

// 有值，不用預設值
console.log(name3); // 'ABCD'
```

### 6. 展開運算子也可以合併多個陣列（對應 `Array.concat()`）

> 展開運算符是把一個陣列展開成個別的值的速寫語法。
> *[ref.: 展開運算符(Spread Operator)](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/rest_spread.html)*

```javascript=
const nameListA = ['Lun', 'Chun'];
const nameListB = ['Yoyo', 'Keke'];

const newNameList = [...nameListA, ...nameListB];
console.log(newNameList);
// ['Lun', 'Chun', 'Yoko', 'Keke'];

// 也可以用  Array.concat() 達到同樣的效果
const nameListA = ['Lun', 'Chun'];
const nameListB = ['Yoyo', 'Keke'];
const newNameList = nameListA.concat(nameListB);

console.log(newNameList);
// ['Lun', 'Chun', 'Yoko', 'Keke'];
```

如果兩個以上的陣列就直接 `...,`：

```javascript=
const newList = [...arr1, ...arr2, ...arr3];

// 當然也可以用 Array.concat()
const newList = Arr1.concat(arr2, arr3);
```

### 把第一個字母改成大寫

```javascript=
const name = 'lun';

function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

const correctName = capitalizeFirstLetter(name); // 'Lun'
```

透過展開運算符合併兩個陣列後拆除陣列：

```javascript=
const name = 'lun';

function capitalizeFirstLetter(string) {
  return [...string[0].toUpperCase(), ...string.slice(1)].join('');
  // string -> ['l', 'u', 'n']，因為使用展開運算符直接分割字串 + 合併
}

const corretName = capitalizeFirstLetter(name); // 'Lun'
```

*[ref. Split String Using ES6 Spread](https://www.samanthaming.com/tidbits/12-split-string-using-spread/)*

### 7. 搭配函式使用

```javascript=
const sumAndMultiply = (a, b) => {
  return [a+b, a*b];
};

const [sum, multiply, text = 'ABCDE'] = sumAndMultiply(2, 5);
console.log(sum); // 7
console.log(multiply); // 10
console.log(text); // 'ABCDE'
```

### 8. 展開運算符也可以把一個字串分割成數個字元

```javascript=
const pizza = 'pizza';

console.log(pizza.split('')); // ['p', 'i', 'z', 'z', 'a']
console.log([...pizza]); // ['p', 'i', 'z', 'z', 'a']
console.log(Array.from(pizza)); // ES6 的 Array.from() -> Array instance from an iterable or array-like object. 
```

但是如果想要以特別的 string 當作切割點還是需要 `string.split()` 啦。


## 物件解構

基本語法：

```javascript=
const { variable1, variable2 } = { variable1: 'Hello', variable2: 'World' };

console.log(variable1); // 'Hello'
console.log(variable2); // 'World'
```

### 1. 直接取物件值，且順序沒關係

鏡子（=）的左邊的 key name，必須與物件內想要取出的 property 之 key name 相等（巢狀時必須結構相同），才可以取得該值：

```javascript=
const options = { width: 200, height: 200, name: 'Card' };

const { width, name, height } = options;
// -> 也可以 { name, height, width } = options; 因為順序沒關係

console.log(width); // 200
console.log(height); // 200
console.log(name); // 'Card'
```

### 2. 如果原本物件內無鏡射出的 key name，則回傳 `undefined`

```javascript=
const { name, age } = {};

console.log(name); // undefined
console.log(age); // undefined
```

### 3. 如果沒值就使用預設值

與陣列解構一樣，有設定預設值在找不到值的情況下就會自動使用預設值，但有找到就會用找到的值：

```javascript=
const { name = 'Lun', age = 24 } = {};

console.log(name); // 'Lun'
console.log(age); // 24

const { name1: 'Yoyo', age = 25 } = { name1: 'Lun', age: 24 }:

// 有值，所以不用預設值
console.log(name1); // 'Lun'
console.log(age); // 24
```

### 4. 直接解構出來給新的變數名字

可以把值從物件解構出來的同時再給它新的變數名字：

```javascript=
const person = {
  name: 'Lun',
  age: 24
};

// 把 name 從 person 解構出來並給新的變數名字 lastName
// age 同上
const { name: lastName, age: myAge } = person;
console.log(lastName); // 'Lun'
console.log(myAge); // 24

console.log(name); // ReferenceError, name is not defined
```

冒號（:）代表 what（從物件解構出來的值）: go where（新的變數名稱），所以我們就可以以新的變數名稱讀取該值，**但是就不能用 what（原本物件的中該值的 key name）讀取，因為已經給它新的名字了**。

當然也可以給預設值：

```javascript=
const person = {};
const { name: lastName = 'Lun' } = person;

console.log(lastName); // Lun
```

### 5. 當然也可以用其餘運算子得到解構剩下的物件

剩下的物件會是新的 address，所以更動剩下的物件並不會修改原本的物件：

```javascript=
const person = {
  name: 'Lun',
  age: 24,
  location: {
    country: 'TW',
    city: 'New Taipei'
  }
};

const { name: lastName, ...restProps } = person;

console.log(lastName); // 'Lun'
console.log(restProps);
// {
//   age: 24,
//   location: {
//     country: 'TW',
//     city: 'New Taipei'
//   }
// }

restPorps.age = 25;
console.log(restProps);
// {
//   age: 25,
//   location: {
//     country: 'TW',
//     city: 'New Taipei'
//   }
// }
console.log(person); // {name: ..., age: 24, location: ...}
```

### 6. 巢狀的物件也可以解構，但是結構要對好

1. 因為 `options` 內無 `title` property，所以解構後可以取預設值 `Menu`
2. `size` 內有兩個 `properties`（`width` 及 `height`）被更改名稱為 `w` 及 `h`
3. `items` 為陣列，用 `item1` 及 `item2` 解構取得值 `Ceke` 及 `Donut`

```javascript=
// ref. https://javascript.info/destructuring-assignment

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// destructuring assignment split in multiple lines for clarity
let {
  size: { // put size here
    width: w,
    height: h
  },
  items: [item1, item2], // assign items here
  title = "Menu" // not present in the object (default value is used)
} = options;

alert(title);  // Menu
alert(w);  // 100
alert(h); // 200
alert(item1);  // Cake
alert(item2);  // Donut
```

### 7. 可以合併兩個物件做到與 `Object.assign()` 一樣的事情，但讚的是不會改變原來的 target 物件

如果想要合併兩個物件，未使用解構的話可以用 `Object.assign(target, source)`，該方法會回傳 target 更動後的樣子（會有 source 的 properties，且如 target 及 source 都有相同的 properties，後面的會蓋掉前面的）。

```javascript=
// ref. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

// target 被更改了 QQ
// 有相同的 property b，後面的（source）會蓋掉前面的（target）
// b: 2 -> b: 4
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
```

但是用其餘運算子合併兩個物件，並不會像 `Object.assign(target, source)` 一樣，改變 target 的物件：

```javascript=
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const result = {...target, ...source};

console.log(result); // { a: 1, b: 4, c: 5 }
// 讚啦，我不會被改變唷
console.log(target); // { a: 1, b: 2}
```

### 2023/03/17 更新

其實 `Object.assign()` 也不會改變原本的物件：

```javascript=
Object.assign(target, ...sources)

// 會回傳改變後的第一位物件

// 最後會得到更改後的 target 物件
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const combined = Object.assign({}, target, source) // {a: 1, b: 4, c: 5}
console.log(target) // { a: 1, b: 2 }

// 但如果以原本的物件為 target，那麼會改變也是正常的
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const combined = Object.assign(target, source) // {a: 1, b: 4, c: 5}
console.log(target) // {a: 1, b: 4, c: 5} -> 因為把 target 放在第一位了
```

## 聰明地以物件解構的方式帶入引數呼叫函式

假設一個函式需要帶入很多引數呼叫，如果不使用物件解構的方式，會造成需要一直記得引數順序的窘境：

```javascript=
// ref.: https://javascript.info/destructuring-assignment

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  // ...
}

// 如果有些引數不需要帶入的話就會變成需要寫一些 falsy value 判斷 + 佔格子排順序
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"]
```

只要宣告函式時有使用物件解構，就可以用物件的方式帶入引數，就不用管引數順序的問題了：

```javascript=
const options = {
  title: 'Hello',
  width: 200,
  height: 200,
  items: ['item1', 'item2']
};

// 宣告函式時直接物件解構
function showMenu({ title, width, height, items }) {
  console.log(title);
  console.log(width);
  console.log(height);
  console.log(items);
}

showMenu(options);
// 'Hello'
// 200
// 200
// ['item1', 'item2']
```

==2022/09/05 更新==
解構不是給預設值，如果呼叫該函式沒有給物件的話還是會報錯：

```javascript=
showMenu();
// TypeError: Cannot destructure property 'title' of 'undefined' as it is undefined.
```

如果想要維持不給引數叫用但不會出錯的話，就必須給預設值：

```javascript=
const options = {
  title: 'Hello',
  width: 200,
  height: 200,
  items: ['item1', 'item2']
};

// 宣告函式時直接物件解構
function showMenu({ title, width, height, items } = {}) {
  console.log(title);
  console.log(width);
  console.log(height);
  console.log(items);
}

showMenu(); // 安全，因為有給預設值 {}
// undefined
// undefined
// undefined
// undefined
```

當然也可以給解構新的變數名字：

```javascript=
const options = {
  title: 'Hello',
  width: 200,
  height: 200,
  items: ['item1', 'item2']
};

function showMenu({ title, width: w, height: h, items: list }) {
  console.log(title);
  console.log(w); // width -> w
  console.log(h); // height -> h
  console.log(list); // items -> list
}

showMenu(options);
// 'Hello'
// 200
// 200
// ['item1', 'item2']
```

更猛的是可以給預設值，如果有需要特定的情況再帶引數進來就好，要不然就都使用預設值：

```javascript=
function showMenu({ title = 'Hello', height = 200, width = 200, items = ['item1', 'item2'] }) {
  console.log(title);
  console.log(height);
  console.log(width);
  console.log(items);
}

showMenu({});
// 'Hello'
// 200
// 200
// ['item1', 'item2']

showMenu({ title: 'Error' });
// 'Error'
// 200
// 200
// ['item1', 'item2']
```

## 總結

1. 基本的解構語法：

```javascript=
const [arr1, arr2] = array;
const {variable1, variable2} = {vairable1: 'Hello', variable2: 'World!'};
```

2. 如果解構的變數名稱在原本的東西找不到/對不到不會報錯，而是回傳 `undefined`
3. 巢狀的物件解構沒問題，只要結構對就好
4. 如果一個函式需要很多參數，最好在宣告的時候就用物件結構，那麼在呼叫的時候只要帶物件（**物件內的 properties 需有相同的 key name**），就不用管順序的問題
5. 函式也可以用預設值 + 物件解構，讓呼叫時帶引數時有更大的便利性

## 參考資料
1. [Why Is Array/Object Destructuring So Useful And How To Use It](https://www.youtube.com/watch?v=NIq3qLaHCIs)
2. [My Boss: You Know ES6, But Why Don’t You Use It? 😠](https://javascript.plainenglish.io/my-boss-you-know-es6-but-why-dont-you-use-it-5e0316f14c67)
3. [Destructuring assignment](https://javascript.info/destructuring-assignment)
4. [展開運算符與其餘運算符](https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/rest_spread.html)