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