解構(destructuring)是一種賦值語法,可從數組中提取元素或從對象中提取屬性,將其值賦給對應的變量或另一個對象的屬性。解構地目的是簡化提取數據的過程,增強代碼的可讀性。有兩種解構語法,分別是數組解構和對象解構,兩者的區別在于解構賦值語句的左側,前者是數組字面量,而后者是對象字面量。為了說明解構的優勢,下面用一個例子來對比手動賦值和解構賦值。
~~~
var arr = [1, 2],
obj = { a: 3, b: 4 },
x, y, a, b;
x = arr[0];
y = arr[1];
a = obj.a;
b = obj.b;
var [x, y] = [1, 2]; //數組解構
var { a, b } = { a: 3, b: 4 }; //對象解構
~~~
  從上面的代碼中可看出,解構賦值只需2條語句,就能完成7條左右的語句才能實現的手動賦值。接下來會先介紹兩種解構語法都包含的通用特性,然后分別講解兩者所獨有的特性,再對它們的兩個特性做對比,最后會分析一種應用解構的特殊場景:函數中的參數解構。
## 一、通用特性
  解構賦值的語句可以包含或忽略聲明關鍵字(例如var、let等),如果包含,那么就需要初始化,否則會報語法錯誤,下面是兩種不正確的寫法。
~~~
var [x, y];
var { a, b };
~~~
  如果忽略聲明關鍵字,那么在運行對象解構的時候,必須用圓括號包裹賦值表達式,而數組解構則并不強制,如下所示。
~~~
[x, y] = [1, 2]; //無圓括號的數組解構
({ a, b } = { a: 3, b: 4 }); //有圓括號的對象解構
~~~
  之所以要用圓括號包裹,是因為表達式左側的花括號會被解析成代碼塊而不是對象字面量。如果把代碼塊和等號運算符放在一行,那么就會報語法錯誤。
  當執行解構語句時,如果等號左側的變量或對象屬性沒有從右側找到對應的數組元素或對象屬性,那么就會被賦為undefined,如下所示。
~~~
[c, d] = [1];
({ e, f } = { e: 3 });
console.log(d); //undefined
console.log(f); //undefined
~~~
## 二、數組解構
**1)位置**
  在數組解構時,解構會按順序作用于數組的元素上,也就是說,變量或對象屬性要取誰的值與它所在的位置有關。下面是一個交換變量位置的例子。
~~~
[x, y] = [1, 2];
console.log(x, y); //1 2
[y, x] = [1, 2];
console.log(x, y); //2 1
~~~
  位置交換后,變量被賦的值也會隨之改變。如果將變量替換為對象屬性,那么得到的結果也是相同的,如下所示。
~~~
var obj = { x, y };
[obj.x, obj.y] = [1, 2];
console.log(obj.x, obj.y); //1 2
[obj.y, obj.x] = [1, 2];
console.log(obj.x, obj.y); //2 1
~~~
  在經典的冒泡排序中,會交換兩個值的位置,傳統的做法是用一個臨時變量做中轉。而如果用解構的語法,那么就可以省略那個臨時變量,并且寫法更為簡潔。兩者的對比如下所示。
~~~
var x = 1,
y = 2;
[x, y] = [y, x]; //數組解構
var tmp = x; //傳統做法
x = y;
y = tmp;
~~~
**2)賦值**
  數組解構還可以有選擇性的賦值,只要在數組指定的位置上不提供元素,就能為其省去解構賦值。下面是一個實現的例子,只為數組的第3個元素提供變量名,在此之前只有兩個用于占位的逗號。
~~~
[, , z] = [1, 2, 3];
console.log(z); //3
~~~
  解構賦值表達式的右側必須是可迭代對象,否則就會報錯。下面的語句都是不正確的。
~~~
[x, y] = NaN;
[x, y] = undefined;
[x, y] = null;
~~~
  前面[第2篇](https://www.cnblogs.com/strick/p/10172721.html)中曾用擴展運算符處理數組,但只在賦值表達式的右側使用了擴展運算符。其實,還可以把擴展運算符放到表達式的左側,如下所示。
~~~
[x, ...y] = [1, 2, 3];
console.log(x); //1
console.log(y); //[2,3]
~~~
  在上面的代碼中,可將...y稱為剩余元素。右側數組的第一個元素賦給了x變量,剩下的兩個元素被收集起來賦給了y數組。注意,剩余元素必須是數組的最后一個元素,并且后面不能有逗號。
## 二、對象解構
**1)賦值**
  由于對象的屬性沒有按順序排列,所以解構對象只會根據屬性的名稱是否相同來取值。下面代碼的第一行是對象解構的簡寫形式,其效果相當于第二行語句。
~~~
({ a, b } = { b: 1, a: 2 });
({ a: a, b: b } = { b: 1, a: 2 });
~~~
  用上面的代碼來描述對象解構的原理,步驟是:先找到同名屬性(冒號左邊的a或b),然后把各自的屬性值對應起來,等號左側的屬性值(冒號右邊的a或b)表示賦值目標,等號右側的屬性值(1或2)表示要提取的值。由此可知,匹配同名屬性只是為了定位,真正被賦值的是處在屬性值位置上的變量或另一個對象的屬性。下面會用不同名稱(即應用別名)的變量和屬性來執行對象解構。
~~~
({ a: e, b: f } = { b: 5, a: 6 });
console.log(e, f); //6 5
var obj = { e, f };
({ a: obj.e, b: obj.f } = { b: 5, a: 6 });
console.log(obj.e, obj.f); //6 5
~~~
**2)屬性**
  對象解構允許出現多個同名屬性,如下代碼所示,等號左側的對象中雖然包含了兩個a屬性,但兩個變量e和f都被成功賦了值。
~~~
({ a: e, a: f } = { b: 5, a: 6 });
console.log(e, f); //6 6
~~~
  ES6允許對象字面量的屬性名用表達式定義(即屬性名可計算),語法也很簡單,只需將屬性名用方括號包裹。對于這類屬性,對象解構也能正確執行,如下所示。
~~~
var obj = { preName: "strick" },
attr = "Name";
({ ["pre" + attr]: value } = obj); //屬性名是表達式
console.log(value); //"strick"
~~~
## 四、特性對比
  數組解構和對象解構都能包含一個可供賦值的默認值。如果是數組解構并且指定位置的元素不存在或其值不存在,那么就會使用默認值。判斷元素的值是否存在,只要與undefined做全等(===)比較,當結果為真時,表示值不存在。下面的三個賦值表達式都包含了默認值,分別表示三種數組解構的情況。
~~~
[x, y=2] = [1];
console.log(y); //2
[x, y=2] = [1, undefined];
console.log(y); //2
[x, y=2] = [1, null];
console.log(y); //null
~~~
**1)默認值**
  對象解構使用默認值的判斷依據是屬性或屬性值是否存在,屬性值與前面的元素值一樣,也要與undefined做全等比較。下面是三種對象解構的情況,解構條件與上面的數組解構類似。
~~~
({ a, b=2 } = { a: 1 });
console.log(b); //2
({ a, b=2 } = { a: 1, b: undefined });
console.log(b); //2
({ a, b=2 } = { a: 1, b: null });
console.log(b); //null
~~~
  上面代碼使用了對象解構的簡寫形式,如果要為別名變量提供默認值,那么就要在別名變量右邊加等號,如下所示。
~~~
({ a, b: digit=2 } = { a: 1 });
console.log(digit); //2
~~~
**2)嵌套解構**
  數組解構和對象解構都支持嵌套解構,這是解構的一種復雜應用,可深入到嵌套的對象或數組中提取對應的數據,下面是兩種語法的嵌套解構。
~~~
[x, [y], z] = [1, [2, 3], 4];
console.log(x, y, z); //1 2 4
({ a: { b: digit } } = { a: { b: 1 } });
console.log(digit); //1
~~~
  在上面代碼的第一條語句中,數組的第二個元素也是數組,即一個數組嵌套了另一個數組。y變量被賦予了內嵌數組的第一個元素,注意,沒有把內嵌數組賦給y變量。在第三條語句中,對象的a屬性值也是對象,即一個對象嵌套了另一個對象,digit變量被賦予了內嵌對象的b屬性的值。
  數組的元素可以是對象,而對象的屬性也可以是數組,如果把數組和對象混合在一起,那么就可以組成更為復雜的混合解構,如下所示。
~~~
({ a: [b] } = { a: [1] });
console.log(b); //1
~~~
## 五、參數解構
  參數解構不但擁有前面兩種解構的所有能力,并且能從實參對象中提取數據,賦給函數體中的同名變量。在講解參數解構之前,先來了解一種代碼優化。當函數要接收大量的參數時,為了增加維護性和擴展性,通常都會用對象代替命名參數,對象的屬性就是函數的參數,如下所示。
~~~
function func1(info) {
console.log(info.name);
console.log(info.age);
}
func1({ name: "strick", age: 29 });
~~~
  雖然減少了參數的數量,但同時也降低了可讀性。如果函數沒有為對象的屬性添加注釋,那么只能通過閱讀函數體中的代碼來理解其含義。而換成參數解構的函數聲明后,屬性的含義就能一目了然,具體如下所示。
~~~
function func2({ name, age }) {
console.log(name);
console.log(age);
}
func2({ name: "strick", age: 29 });
~~~
  在上面的代碼中,函數的參數是一個對象,當然,也可以把參數換成數組,利用[第2篇](https://www.cnblogs.com/strick/p/10172721.html)中的剩余參數來實現相同效果的參數解構,具體如下所示。
~~~
function func3(...[name, age]) {
console.log(name);
console.log(age);
}
func3("strick", 29);
~~~
**1)限制**
  如果參數解構的目標是對象,那么就會有一個限制。這個限制就是調用函數必須傳遞該對象,否則會拋出異常。注意,下面是錯誤的寫法。
~~~
func2();
~~~
  若要避免拋出異常,可為參數定義默認值(這是ES6新增的函數特性),如下所示。
~~~
function func4({ name, age } = {}) {
console.log(name);
console.log(age);
}
func4();
~~~
**2)默認值對比**
  當解構默認值和參數默認值結合應用時,兩者之間會有一些微妙的差別。下面是兩個函數,func5()函數包含解構默認值,func6()函數包含參數默認值。
~~~
function func5({ name = "strick" } = {}) { //解構默認值
console.log(name);
}
function func6({ name } = { name: "freedom" }) { //參數默認值
console.log(name);
}
~~~
  下面是兩組函數調用的對比,第一組省略了函數的參數,第二組傳入的參數值為undefined。
~~~
func5(); //"strick"
func6(); //"freedom"
func5(undefined); //"strick"
func6(undefined); //"freedom"
~~~
  根據上面輸出的結果可知,name變量被賦為解構默認值或參數默認值。這是因為如果函數的參數省略或其值為undefined,那么就會使用參數的默認值。由于func6()函數的參數指定了默認值(即定義了name屬性),因此對象解構后,name變量被賦值為“freedom”。而func5()函數的參數默認值是空對象,得再根據對象解構默認值的規則才能得出name變量的值,最終name變量會被賦值為“strick”。
  下面是傳入空對象的情況,兩個函數中的參數都會被賦為空對象。在對象解構的時候,由于func5()函數的參數包含解構默認值,因此name變量被賦值為“strick”;而func6()函數的參數并不包含解構默認值,因此name變量的值為undefined。
~~~
func5({}); //"strick"
func6({}); //undefined
~~~
*****
> 原文出處:
[博客園-ES6躬行記](https://www.cnblogs.com/strick/category/1372951.html)
[知乎專欄-ES6躬行記](https://zhuanlan.zhihu.com/pwes6)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020