1. 程式人生 > 實用技巧 >變數的解構賦值

變數的解構賦值

1. 陣列的結構賦值


ES6允許從陣列中題取值,按照對應位置,為變數賦值。陣列的解構賦值存在一些特殊的情況:

  • 解構不成功,變數的值等於undefined;
  • 不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的陣列(右邊比左邊多)。這時解構依然可以成功;
  • 等號右邊是不可遍歷的結構(轉化為物件之後不具有Iterator介面),解構報錯。

事實上,只要具有Iterator介面,都可以採用陣列形式的解構賦值。比如下面這個例子:

//Generator函式
function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

解構賦值允許設定預設值。ES6內部使用嚴格相等運算子(===)判斷一個位置是否有值,所以只有當一個數組成員嚴格等於undefined時,預設值才會生效。

對於預設值(用=指定),有以下兩個注意點:

  • 如果預設值是一個表示式,表示式是惰性求值的,即要用到時才會求值;
  • 預設值可以引用解構賦值的其他變數,前提是該變數必須已經被宣告。

2. 物件的解構賦值


物件與陣列的區別在於,物件的屬性沒有順序,所以必須變數與物件的屬性同名,才能取到正確的值(順序無影響)。如果解構失敗,變數的值同樣為undefined。

物件的解構賦值,可以很方便的將現有物件的方法,賦值到某個變數:

//將Math物件的對數,正弦,餘弦三個屬性賦值給對應的變數
let { log, sin, cos } = Math;

//將console.log賦值到log變數
let { log } = console;
log("Hello");    //Hello

✨變數名如果與屬性名不一致,則要增加模式匹配欄位。

//變數與屬性名不匹配,報錯
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz //undefined

//使用模式匹配欄位,正確
let { foo: baz } = {foo: 'aaa', bar: 'bbb' };
baz //'aaa'

實際上,變數與屬性名一致的情況實際上是省略了模式匹配欄位。這也表明物件解構的內部機制是,先找到同名屬性,再匹配給對應變數。被賦值的實際上是後者。

✨與陣列一樣,物件的解構也可以用於巢狀的物件。這種情況有三個需要注意的地方:

  • 注意解構物件中究竟是變數還是模式。
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

//這裡的p是模式
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

//這裡的p也被作為變數賦值了
let { p, p: [x, { y }] } = obj;
p // ["Hello", {y: "World"}]
  • 巢狀賦值可以賦值給物件的某一個屬性/陣列的某一個項。
let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]
  • 解構的巢狀的物件,如果取得到子屬性,但父屬性不存在,也會報錯。
// 報錯,因為此時父屬性foo為undefined,再取子屬性就會報錯
let {foo: {bar}} = {baz: 'baz'};
  • 物件的解構賦值可以取到繼承的屬性。
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);   //obj2是obj1的原型物件(父物件)

const { foo } = obj1;
foo // "bar"(依舊可以取到父物件的屬性)

和陣列一樣,物件的解構賦值也可以設定預設值(用=指定),同樣需要物件的屬性值嚴格等於undefined。

✨物件的解構賦值有三個注意點:

  • 將已經宣告的變數進行解構賦值要小心,不能出現大括號開頭的情況,因為這樣ES6會把它理解為一個程式碼塊而不是物件。需要把整個語句放在一對圓括號裡面才能正常執行。
  • 解構賦值允許等號左邊的模式中不放任何的變數名。這樣雖然毫無意義,但卻是合法可執行的。
  • 由於陣列的本質是特殊的物件,因此可以對陣列進行物件屬性的解構。這種寫法屬於“屬性名錶達式”。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

3. 字串,數值和布林值的解構賦值


解構賦值的規則是,只要等號右邊不是陣列或物件,就先轉換為物件。由於undefined和null無法轉換為物件,所以對他們解構賦值都會報錯。

  • 字串被轉化為一個類似陣列的物件。他還有一個length屬性,這個屬性也可以用於解構賦值。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5
  • 數值和布林值可以取到包裝物件的屬性值。
let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

4. 函式引數的解構賦值


function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

在上面的程式碼中,雖然傳入的引數是陣列形式的,但實際上陣列引數在傳入的瞬間被解構為變數x和y。對函式內部程式碼來說,他們能感受到的就是引數x和y。

函式引數的解構也可以使用預設值,但是要注意區分是變數的預設值還是引數物件的預設值。

//使用 "=" 設定的是變數的預設值
function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

//使用 ":" 設定的是物件的預設值
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

5. 圓括號問題


解構賦值雖然使用起來很方便,但解析起來很麻煩。對編譯器來說,無法從一開始就知道究竟是模式和表示式。這使得圓括號的處理變得麻煩起來。ES6的規則是,只要有可能引起解構的歧義,就不使用圓括號。由於這條規則的複雜性,建議只要有可能,就不要在模式中放置圓括號。

  • 不能使用圓括號的情況:變數宣告語句、函式引數、賦值語句的模式。
  • 可以使用的情況:賦值語句的非模式部分。

6. 解構賦值的用途


  1. 交換變數的值,可以使用 [x, y] = [y, x]; 這種簡明的語句了;
  2. 函式如果要返回多個值,只能把他們放在一個數組或物件中返回,有了解構賦值,取出這些值就變得非常簡單;
  3. 解構賦值可以很方便的將函式的引數與變數對應起來;
  4. 提取JSON資料方便了很多;
  5. 指定函式引數的預設值方便了很多;
  6. Map 結構原生支援 Iterator 介面,配合變數的解構賦值,獲取鍵名和鍵值就非常方便;
  7. 載入模組時,往往需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。