1、數組的解構指派
基本用法:
ES6允許按照一定模式,從數組和對象中提取值,對變量進行指派,這被稱為解構(Destructuring)。
以前,為變量指派,隻能直接指定值。
<code>var</code> <code>a = 1;</code>
<code>var</code> <code>b = 2;</code>
<code>var</code> <code>c = 3;</code>
ES6允許這樣寫:
<code>var</code> <code>[a,b,c] = [1,2,3];</code>
<code>console.log(a); </code><code>//1</code>
<code>console.log(b); </code><code>//2</code>
<code>console.log(c); </code><code>//3</code>
上面代碼表示,可以從數組中提取值,按照對應位置,對變量指派。
本質上,這種寫法屬于“模式比對”,隻要等号兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
<code>let[foo, [[bar],[baz]]] = [1, [[2],[3]]];</code>
<code>console.log(foo); </code><code>//1</code>
<code>console.log(bar); </code><code>//2</code>
<code>console.log(baz); </code><code>//3</code>
<code>let[,two,] = [,</code><code>'second'</code><code>,];</code>
<code>console.log(two); </code><code>//second</code>
<code>let[x,y] = [4,5,6];</code>
<code>console.log(x); </code><code>//4</code>
<code>console.log(y); </code><code>//5</code>
<code>let[head, ...foot] = [1,2,3,4]</code>
<code>console.log(head); </code><code>//1</code>
<code>console.log(foot); </code><code>//[2,3,4]</code>
<code>let[i1,i2,...ix] =[6];</code>
<code>console.log(i1); </code><code>//6</code>
<code>console.log(i2); </code><code>//undefined</code>
<code>console.log(ix); </code><code>//[]</code>
如果解構不成功,變量的值就等于undefined。
<code>var</code> <code>[foo2]=[];</code>
<code>console.log(foo2); </code><code>//undefined</code>
<code>var</code> <code>[bar3,foo3]=[1];</code>
<code>console.log(bar3); </code><code>//1</code>
<code>console.log(foo3); </code><code>//undefined</code>
以上兩種情況都屬于解構不成功,foo的值都會等于undefined。
另一種情況是不完全解構,即等号左邊的模式,隻比對一部分的等号右邊的數組。這種情況下,解構依然可以成功。
<code>let[x,y]=[1,2,3];</code>
<code>console.log(x); </code><code>//1</code>
<code>console.log(y); </code><code>//2</code>
<code>let[a,[b],c] = [1,[2,3,4],5];</code>
<code>console.log(c); </code><code>//5</code>
上面兩個例子,都屬于不完全解構,但是可以成功。
如果等号的右邊不是數組(或者嚴格地說,不是可周遊的結構),那麼将會報錯。
<code>let[a]=1;</code>
<code>let[b]=</code><code>false</code><code>;</code>
<code>let[c]=NaN;</code>
<code>let[d]=undefined;</code>
<code>let[e]=</code><code>null</code><code>;</code>
<code>let[f]={};</code>
<code>console.log(a+b+c+d+e+f); </code><code>//Uncaught TypeError: undefined is not a function</code>
上面的表達式都會報錯,因為等号右邊的值,要麼轉為對象以後不具備Iterator接口(前五個表達式),要麼本身就不具備Iterator接口(最後一個表達式)。
解構指派不僅适用于var指令,也适用于let和const指令。
<code>var</code> <code>[v1, v2, ..., vN ] = array;</code>
<code>let [v1, v2, ..., vN ] = array;</code>
<code>const [v1, v2, ..., vN ] = array;</code>
對于Set結構,也可以使用數組的解構指派。
<code>let[x,y,z]=</code><code>new</code> <code>Set([</code><code>'a'</code><code>,</code><code>'b'</code><code>,</code><code>'c'</code><code>]);</code>
<code>console.log(x); </code><code>//a</code>
<code>console.log(y); </code><code>//b</code>
<code>console.log(z); </code><code>//c</code>
事實上,隻要某種資料結構具有Iterator接口,都可以采用數組形式的解構指派。
<code>function</code><code>* fibs(){</code>
<code> </code><code>var</code> <code>a=0;</code>
<code> </code><code>var</code> <code>b=1;</code>
<code> </code><code>while</code><code>(</code><code>true</code><code>){</code>
<code> </code><code>yield a;</code>
<code> </code><code>[a,b]=[b,a+b];</code>
<code> </code><code>}</code>
<code>}</code>
<code>var</code><code>[first,second,third,fourth,fifth,sixth]=fibs();</code>
<code>console.log(fifth); </code><code>//3</code>
<code>console.log(sixth); </code><code>//5</code>
上面代碼中,fibs是一個Generator函數,原生具有Iterator接口。解構指派會依次從這個接口擷取值。
預設值
解構指派允許指定預設值
<code>var</code> <code>[foo=</code><code>false</code><code>]=[];</code>
<code>console.log(foo); </code><code>//false</code>
<code>var</code> <code>[x,y=</code><code>'b'</code><code>]=[</code><code>'a'</code><code>];</code>
<code>var</code> <code>[j,k=</code><code>'b'</code><code>]=[</code><code>'a'</code><code>,undefined];</code>
<code>console.log(j); </code><code>//a</code>
<code>console.log(k); </code><code>//b</code>
注意,ES6内部使用嚴格相等運算符(===),判斷一個位置是否有值。是以,如果一個數組成員不嚴格等于undefined,預設值是不會生效的。
<code>var</code> <code>[x=1] = [undefined];</code>
<code>var</code> <code>[y=1] = [</code><code>null</code><code>];</code>
<code>console.log(y); </code><code>//null</code>
上面代碼中,如果一個數組成員是null,預設值就不會生效,因為null不嚴格等于undefined。
如果預設值是一個表達式,那麼這個表達式是惰性求值的,即隻有在用到的時候,才會求值。
<code>function</code> <code>f(){</code>
<code> </code><code>console.log(</code><code>'aaa'</code><code>);</code>
<code>let[x=f()] = [1];</code>
<code>f(); </code><code>//aaa</code>
第一段代碼中,因為x能取到值,是以函數f根本不會執行。上面的代碼其實等價于下面的代碼。
<code>let x;</code>
<code>if</code> <code>([1][0] === undefined) {</code>
<code> </code><code>x = f();</code>
<code>} </code><code>else</code> <code>{</code>
<code> </code><code>x = [1][0];</code>
預設值可以引用解構指派的其他變量,但該變量必須已經聲明。
<code>let[a=1, b=a] =[];</code>
<code>console.log(b); </code><code>//1</code>
<code>let[a=1, b=a] =[2];</code>
<code>console.log(a); </code><code>//2</code>
<code>let[a=1, b=a] =[2,3];</code>
<code>console.log(b); </code><code>//3</code>
<code>let[a=b, b=1] =[]; </code><code>//Uncaught ReferenceError: b is not defined</code>
上面最後一個表達式之是以會報錯,是因為a用到預設值b時,b還沒有聲明。
2、對象的解構指派
解構不僅可以用于數組,還可以用于對象。
<code>var</code> <code>{a,b} = {a:</code><code>'apple'</code><code>, b:</code><code>'banana'</code><code>};</code>
<code>console.log(a); </code><code>//apple</code>
<code>console.log(b); </code><code>//banana</code>
對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正确的值。
<code>var</code> <code>{b,a} = {a:</code><code>'apple'</code><code>, b:</code><code>'banana'</code><code>};</code>
<code>var</code> <code>{c} = {a:</code><code>'apple'</code><code>, b:</code><code>'banana'</code><code>};</code>
<code>console.log(c); </code><code>//undefined</code>
上面代碼的第一個例子,等号左邊的兩個變量的次序,與等号右邊兩個同名屬性的次序不一緻,但是對取值完全沒有影響。第二個例子的變量沒有對應的同名屬性,導緻取不到值,最後等于undefined。
如果變量名與屬性名不一緻,必須寫成下面這樣。
<code>var</code> <code>{a:c} = {a:</code><code>'apple'</code><code>, b:</code><code>'banana'</code><code>};</code>
<code>console.log(c); </code><code>//apple</code>
<code>let obj = {first:</code><code>'hello'</code><code>, last:</code><code>'world'</code><code>};</code>
<code>let {first:f, last:l} = obj;</code>
<code>console.log(f); </code><code>//hello</code>
<code>console.log(l); </code><code>//world</code>
這實際上說明,對象的解構指派是下面形式的簡寫
<code>var</code> <code>{ foo: foo, bar: bar } = { foo: </code><code>"aaa"</code><code>, bar: </code><code>"bbb"</code> <code>};</code>
也就是說,對象的解構指派的内部機制,是先找到同名屬性,然後再賦給對應的變量。真正被指派的是後者,而不是前者。
<code>var</code> <code>{v:n} = {v:</code><code>'vue'</code><code>, r:</code><code>'react'</code><code>};</code>
<code>console.log(n); </code><code>//vue</code>
<code>console.log(v); </code><code>//Uncaught ReferenceError: v is not defined</code>
<code>//console.log(r); //Uncaught ReferenceError: r is not defined</code>
上面代碼中,v是比對的模式,n才是變量。真正被指派的是變量n,而不是模式v。 注意,采用這種寫法時,變量的聲明和指派是一體的。
對于let和const來說,變量不能重新聲明,是以一旦指派的變量以前聲明過,就會報錯。
<code>let foo;</code>
<code>let {foo} = {foo:1}; </code><code>//Uncaught SyntaxError: Identifier 'foo' has already been declared</code>
<code>let b;</code>
<code>let {a:b} = {a:1}; </code><code>//Uncaught SyntaxError: Identifier 'b' has already been declared</code>
<code>let a;</code>
<code>let {a:b} = {a:1};</code>
<code>console.log(a); </code><code>//undefined</code>
上面代碼中,解構指派的變量都會重新聲明,是以報錯了。不過,因為var指令允許重新聲明,是以這個錯誤隻會在使用let和const指令時出現。如果沒有第二個let指令,上面的代碼就不會報錯。
<code>({foo} = {foo:1});</code>
<code>let baz;</code>
<code>({bar:baz}={bar:1});</code>
<code>console.log(baz); </code><code>//1</code>
上面代碼中,let指令下面一行的圓括号是必須的,否則會報錯。因為解析器會将起首的大括号,了解成一個代碼塊,而不是指派語句。
和數組一樣,解構也可以用于嵌套結構的對象。
<code>var</code> <code>obj ={</code>
<code> </code><code>p:[</code>
<code> </code><code>'hello'</code><code>, {w:</code><code>'world'</code><code>}</code>
<code> </code><code>]</code>
<code>};</code>
<code>var</code> <code>{p:[h,{w}]} = obj;</code>
<code>console.log(h); </code><code>//hello</code>
<code>console.log(w); </code><code>//world</code>
注意,這時p是模式,不是變量,是以不會被指派。
<code>var</code> <code>node ={</code>
<code> </code><code>loc:{</code>
<code> </code><code>start:{</code>
<code> </code><code>line:1,</code>
<code> </code><code>column:5</code>
<code> </code><code>}</code>
<code>var</code> <code>{loc:{start:{line}}} = node;</code>
<code>console.log(line); </code><code>//1</code>
<code>console.log(loc); </code><code>//Uncaught ReferenceError: loc is not defined</code>
<code>console.log(start); </code><code>//Uncaught ReferenceError: start is not defined</code>
上面代碼中,隻有line是變量,loc和start都是模式,不會被指派。
下面是嵌套指派的例子。
<code>let obj = {};</code>
<code>let arr = [];</code>
<code>({foo:obj.prop, bar:arr[0]} = {foo:123, bar:</code><code>true</code><code>});</code>
<code>console.log(obj); </code><code>//Object {prop: 123}</code>
<code>console.log(arr); </code><code>//[true]</code>
對象的解構也可以指定預設值。
<code>var</code> <code>{x=3} = {x:undefined};</code>
<code>console.log(x); </code><code>//3</code>
<code>var</code> <code>{x, y=5} = {x:1};</code>
<code>var</code> <code>{x:y=5} = {};</code>
<code>console.log(x); </code><code>//Uncaught ReferenceError: x is not defined</code>
<code>var</code> <code>{x:y=5} = {x:10};</code>
<code>console.log(y); </code><code>//10</code>
<code>var</code> <code>{message: msg=</code><code>'Hello World!'</code><code>} = {};</code>
<code>console.log(msg); </code><code>//Hello World!</code>
預設值生效的條件是,對象的屬性值嚴格等于undefined。
<code>var</code> <code>{x=3} = {x:</code><code>null</code><code>};</code>
<code>console.log(x); </code><code>//null</code>
上面代碼中,如果x屬性等于null,就不嚴格相等于undefined,導緻預設值不會生效。
如果解構失敗,變量的值等于undefined。
<code>var</code> <code>{foo} = {bar: </code><code>'baz'</code><code>};</code>
<code>console.log(foo); </code><code>//undefined</code>
如果解構模式是嵌套的對象,而且子對象所在的父屬性不存在,那麼将會報錯。
<code>var</code> <code>{foo: {bar}} = {baz:</code><code>'baz'</code><code>};</code>
<code>console.log(bar); </code><code>//Uncaught TypeError: Cannot match against 'undefined' or 'null'.</code>
上面代碼中,等号左邊對象的foo屬性,對應一個子對象。該子對象的bar屬性,解構時會報錯。原因很簡單,因為foo這時等于undefined,再取子屬性就會報錯,請看下面的代碼。
<code>var</code> <code>_tmp = {bar:</code><code>'baz'</code><code>};</code>
<code>console.log(_tmp.foo.bar); </code><code>//Uncaught TypeError: Cannot read property 'bar' of undefined</code>
如果要将一個已經聲明的變量用于解構指派,必須非常小心。
<code>// 錯誤的寫法</code>
<code>var</code> <code>x;</code>
<code>{x} = {x: 1};</code><code>// SyntaxError: syntax error</code>
上面代碼的寫法會報錯,因為JavaScript引擎會将{x}了解成一個代碼塊,進而發生文法錯誤。隻有不将大括号寫在行首,避免JavaScript将其解釋為代碼塊,才能解決這個問題。
<code>// 正确的寫法</code>
<code>({x} = {x: 1});</code>
上面代碼将整個解構指派語句,放在一個圓括号裡面,就可以正确執行。
解構指派允許等号左邊的模式中,不放置任何變量名。是以,可以寫出非常古怪的指派表達式。
<code>({} = [</code><code>true</code><code>, </code><code>false</code><code>]);</code>
<code>({} = </code><code>'abc'</code><code>);</code>
<code>({} = []);</code>
上面的表達式雖然毫無意義,但是文法是合法的,可以執行。
對象的解構指派,可以很友善地将現有對象的方法,指派到某個變量。
<code>let { log, sin, cos } = Math;</code>
上面代碼将Math對象的對數、正弦、餘弦三個方法,指派到對應的變量上,使用起來就會友善很多。
由于數組本質是特殊的對象,是以可以對數組進行對象屬性的解構。
<code>var</code> <code>arr = [1,2,3];</code>
<code>var</code> <code>{0:first, [arr.length-1]:last} = arr;</code>
<code>console.log(first); </code><code>//1</code>
<code>console.log(last); </code><code>//3</code>
上面代碼對數組進行對象解構。數組arr的0鍵對應的值是1,[arr.length - 1]就是2鍵,對應的值是3。方括号這種寫法,屬于“屬性名表達式”
3、字元串的解構指派
字元串也可以解構指派,因為字元串被轉換成了一個類似數組的對象。
<code>const [a,b,c,d,e] = </code><code>'hello'</code><code>;</code>
<code>console.log(a+b+c+d+e); </code><code>//hello</code>
類似數組的對象都有一個length屬性,是以還可以對這個屬性解構指派
<code>let{length:len} = </code><code>'hello'</code><code>;</code>
<code>console.log(len); </code><code>//5</code>
4、數值和布爾值的解構指派
解構指派時,如果等号右邊是數值和布爾值,則會先轉為對象。
<code>let {toString: s} = 123;</code>
<code>console.log(s === Number.prototype.toString)</code><code>// true</code>
<code>let {toString: s} = </code><code>true</code><code>;</code>
<code>console.log(s === Boolean.prototype.toString); </code><code>// true</code>
上面代碼中,數值和布爾值的包裝對象都有toString屬性,是以變量s都能取到值。 解構指派的規則是,隻要等号右邊的值不是對象,就先将其轉為對象。由于undefined和null無法轉為對象,是以對它們進行解構指派,都會報錯。
<code>let { prop: x } = undefined; </code><code>// TypeError</code>
<code>let { prop: y } = </code><code>null</code><code>; </code><code>// TypeError</code>
5、函數參數的解構指派
函數的參數也可以使用解構指派
<code>function</code> <code>add([x,y]){</code>
<code> </code><code>console.log(x+y);</code>
<code>add([1,2]); </code><code>//3</code>
上面代碼中,函數add的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量x和y。對于函數内部的代碼來說,它們能感受到的參數就是x和y。
下面是另一個例子。
<code>[[1, 2], [3, 4]].map(([a, b]) => a + b); </code><code>// [ 3, 7 ]</code>
函數參數的解構也可以使用預設值。
<code>function</code> <code>move({x=0, y=0}={}){</code>
<code> </code><code>console.log([x,y]);</code>
<code>move({x: 3, y: 8}); </code><code>//[3, 8]</code>
<code>move({x: 3}); </code><code>//[3, 0]</code>
<code>move({}); </code><code>//[0, 0]</code>
<code>move(); </code><code>//[0, 0]</code>
上面代碼中,函數move的參數是一個對象,通過對這個對象進行解構,得到變量x和y的值。如果解構失敗,x和y等于預設值。
注意,下面的寫法會得到不一樣的結果。
<code>function</code> <code>move({x, y}={x:0, y:0}){</code>
<code>move({x: 3}); </code><code>//[3, undefined]</code>
<code>move({}); </code><code>//[undefined, undefined]</code>
上面代碼是為函數move的參數指定預設值,而不是為變量x和y指定預設值,是以會得到與前一種寫法不同的結果。
undefined會觸發函數參數的預設值。
<code>[1, undefined, 3].map((x = </code><code>'yes'</code><code>) => x); </code><code>// [ 1, 'yes', 3 ]</code>
6、圓括号問題
解構指派雖然很友善,但是解析起來并不容易。對于編譯器來說,一個式子到底是模式,還是表達式,沒有辦法從一開始就知道,必須解析到(或解析不到)等号才能知道。
由此帶來的問題是,如果模式中出現圓括号怎麼處理。ES6的規則是,隻要有可能導緻解構的歧義,就不得使用圓括号。
但是,這條規則實際上不那麼容易辨識,處理起來相當麻煩。是以,建議隻要有可能,就不要在模式中放置圓括号。
不能使用圓括号的情況
以下三種解構指派不得使用圓括号。
(1)變量聲明語句中,不能帶有圓括号。
<code>// 全部報錯</code>
<code>var</code> <code>[(a)] = [1];</code>
<code>var</code> <code>{x: (c)} = {};</code>
<code>var</code> <code>({x: c}) = {};</code>
<code>var</code> <code>{(x: c)} = {};</code>
<code>var</code> <code>{(x): c} = {};</code>
<code>var</code> <code>{ o: ({ p: p }) } = { o: { p: 2 } };</code>
上面的語句都會報錯,因為它們都是變量聲明語句,模式不能使用圓括号。
(2)函數參數中,模式不能帶有圓括号。
函數參數也屬于變量聲明,是以不能帶有圓括号。
<code>// 報錯</code>
<code>function</code> <code>f([(z)]) { </code><code>return</code> <code>z; }</code>
(3)指派語句中,不能将整個模式,或嵌套模式中的一層,放在圓括号之中。
<code>({ p: a }) = { p: 42 };</code>
<code>([a]) = [5];</code>
上面代碼将整個模式放在圓括号之中,導緻報錯。
<code>[({ p: a }), { x: c }] = [{}, {}];</code>
上面代碼将嵌套模式的一層,放在圓括号之中,導緻報錯。
可以使用圓括号的情況
可以使用圓括号的情況隻有一種:指派語句的非模式部分,可以使用圓括号。
<code>[(b)] = [3]; </code><code>// 正确</code>
<code>({ p: (d) } = {}); </code><code>// 正确</code>
<code>[(parseInt.prop)] = [3]; </code><code>// 正确</code>
上面三行語句都可以正确執行,因為首先它們都是指派語句,而不是聲明語句;其次它們的圓括号都不屬于模式的一部分。第一行語句中,模式是取數組的第一個成員,跟圓括号無關;第二行語句中,模式是p,而不是d;第三行語句與第一行語句的性質一緻。
7、用途
變量的解構指派用途很多。
(1)交換變量的值
<code>[x, y] = [y, x];</code>
上面代碼交換變量x和y的值,這樣的寫法不僅簡潔,而且易讀,語義非常清晰。
(2)從函數傳回多個值
函數隻能傳回一個值,如果要傳回多個值,隻能将它們放在數組或對象裡傳回。有了解構指派,取出這些值就非常友善。
<code>// 傳回一個數組</code>
<code>function</code> <code>example() {</code>
<code> </code><code>return</code> <code>[1, 2, 3];</code>
<code>var</code> <code>[a, b, c] = example();</code>
<code>// 傳回一個對象</code>
<code> </code><code>return</code> <code>{</code>
<code> </code><code>foo: 1,</code>
<code> </code><code>bar: 2</code>
<code> </code><code>};</code>
<code>var</code> <code>{ foo, bar } = example();</code>
(3)函數參數的定義
解構指派可以友善地将一組參數與變量名對應起來。
<code>// 參數是一組有次序的值</code>
<code>function</code> <code>f([x, y, z]) { ... }</code>
<code>f([1, 2, 3]);</code>
<code>// 參數是一組無次序的值</code>
<code>function</code> <code>f({x, y, z}) { ... }</code>
<code>f({z: 3, y: 2, x: 1});</code>
(4)提取JSON資料
解構指派對提取JSON對象中的資料,尤其有用。
<code>var</code> <code>jsonData = {</code>
<code> </code><code>id: 42,</code>
<code> </code><code>status: </code><code>"OK"</code><code>,</code>
<code> </code><code>data: [867, 5309]</code>
<code>let { id, status, data: number } = jsonData;</code>
<code>console.log(id, status, number);</code>
<code>// 42, "OK", [867, 5309]</code>
上面代碼可以快速提取JSON資料的值。
(5)函數參數的預設值
<code>jQuery.ajax = </code><code>function</code> <code>(url, {</code>
<code> </code><code>async = </code><code>true</code><code>,</code>
<code> </code><code>beforeSend = </code><code>function</code> <code>() {},</code>
<code> </code><code>cache = </code><code>true</code><code>,</code>
<code> </code><code>complete = </code><code>function</code> <code>() {},</code>
<code> </code><code>crossDomain = </code><code>false</code><code>,</code>
<code> </code><code>global = </code><code>true</code><code>,</code>
<code> </code><code>// ... more config</code>
<code> </code><code>}) {</code>
<code> </code><code>// ... do stuff</code>
指定參數的預設值,就避免了在函數體内部再寫var foo = config.foo || 'default foo';這樣的語句。
(6)周遊Map結構
任何部署了Iterator接口的對象,都可以用for...of循環周遊。Map結構原生支援Iterator接口,配合變量的解構指派,擷取鍵名和鍵值就非常友善。
<code>var</code> <code>map = </code><code>new</code> <code>Map();</code>
<code>map.set(</code><code>'first'</code><code>, </code><code>'hello'</code><code>);</code>
<code>map.set(</code><code>'second'</code><code>, </code><code>'world'</code><code>);</code>
<code>for</code> <code>(let [key, value] of map) {</code>
<code> </code><code>console.log(key + </code><code>" is "</code> <code>+ value);</code>
<code>// first is hello</code>
<code>// second is world</code>
如果隻想擷取鍵名,或者隻想擷取鍵值,可以寫成下面這樣。
<code>// 擷取鍵名</code>
<code>for</code> <code>(let [key] of map) {</code>
<code> </code><code>// ...</code>
<code>// 擷取鍵值</code>
<code>for</code> <code>(let [,value] of map) {</code>
(7)輸入子產品的指定方法
加載子產品時,往往需要指定輸入那些方法。解構指派使得輸入語句非常清晰。
<code>const { SourceMapConsumer, SourceNode } = require(</code><code>"source-map"</code><code>);</code>
本文轉自 frwupeng517 51CTO部落格,原文連結:http://blog.51cto.com/dapengtalk/1873591