# [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)