ES6系列入門學習記錄:變數的解構賦值

ExplorerCl發表於2019-02-27

前言

年都過去半個月了,我終於又重新開始更新了。雖然只是第二篇,但是我會繼續加油努力,一定不會放棄更新的。在文章中若有什麼不妥或者您有更多建議的話,歡迎和期待您給我留言,您的每一個留言都可能成為我進步的助力,十分感謝。那就廢話不多說直接開始吧。

概念

解構賦值,顧名思義,就是解開一個結構,然後從裡面拿出值用來給變數賦值賦值。所以解構賦值主要是以資料型別來劃分的。

陣列的解構賦值

var [a,b,c] = [1,2,3];
複製程式碼

上述程式碼算是最簡單的陣列解構賦值,其實也可以看做是資料的另一種展示。比如上述程式碼與下面的程式碼其實是一樣的。

var a=1,b=2,c=3;
複製程式碼

所以解構賦值最主要的作用,是可以讓我們簡化提取值的過程。

本質上,這種寫法屬於“模式匹配”,同模式下,左邊的變數就會被賦予對應位置的右邊的值。例如:

let [a,[[b],c]] = [1,[[2],3]];
a //1
b //2
c //3
複製程式碼

並且只要模式相同,即便部分位置的變數或者值為空,依舊可以進行匹配。

let [a, , b] = [1,2,3];
a //1
b //3

let [a,b,c] = [1,2];
a//1
b//2
c//undefind
複製程式碼

當解構不成功,即變數沒有得到賦值,或者直接賦值undefind時,變數的值就會等於undefind

當匹配兩邊的模式相同,且長度不同時,此時的解構賦值被稱為不完全解構。雖然叫不完全解構,但是依舊算解構成功的。

let [a,b] = [1,2,3];
a //1
b //2
複製程式碼

上面一直提到一個前提情況,那就是模式相同,沒錯,這是比較需要注意的一點。當兩邊模式不同時,解構賦值是會報錯的。

let [a] = 1;
let [b] = false;
let [c] = {};
複製程式碼

在不嚴謹的情況下,我們可以說,當兩邊的資料型別不同時,解構賦值會出現報錯。

只要某種資料結構具有Iterator介面,都可以採用陣列形式的解構賦值。Iterator介面最主要的功能是可以提供遍歷命令for...of使用,不難猜測,其實陣列解構賦值是一個將變數遍歷迴圈,然後一一進行賦值的操作

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入門裡面的例項,由於個人目前還不清楚具體哪些資料結構具有Iterator介面,所以這裡直接搬運一下。

預設值

let [a=1] = [];
a//1
複製程式碼

解構賦值操作時,可以設定一個預設值,若解構賦值操作室,對應位置上的值為undefind時,將會給變數賦值預設值。

需要注意的是,ES6內部使用嚴格相等運算子(===)來判斷一個位置是否有值,我一般習慣稱它為全等符號。所以,與一般的判斷不同,這裡只有用於賦值的陣列成員的值為undefined(嚴格等於undefined)時,預設值才會生效。

let [a=1] = [undefined];
a//1

let [b=1] = [null];
b//null

let [c=1] = [NaN];
c//NaN
複製程式碼

使用預設值時,還可以引用解構賦值的其他變數,但前提是該變數已宣告。

let [a=1,b=a] = [];
a//1
b//1

let [a=b,b=1] = [];
//ReferenceError: b is not defined

var [a=b,b=1] = [];
a//undefined
b//1
複製程式碼

物件的解構賦值

let { a,b } = { a:'1',b:'2'};
a //'1'
b //'2'
複製程式碼

物件的解構賦值與陣列的解構賦值最大的不同之處,在於陣列的解構賦值,變數的取值是由位置決定的;而物件的解構賦值,變數的取值是由屬性名來決定的,只有變數與屬性名相同,才可以取到值。

let { a , b } = { b : '2' , a : '1' };
a //1
b //2

let { a } = { b: '1' , c: '2' };
a//undefined
複製程式碼

當變數名與屬性名不一致,卻又需要進行解構賦值時,可以使用變數再進行一次解構賦值。

let obj = { a : '1' , b : '2' };
let { a : c , b : d } = obj;
c //'1'
d //'2'
複製程式碼

並且,在這過程中,實際被賦值的,其實是cd。而ab是模式,起到類似於一箇中介作用,不會被實際賦值。

let { a : b } = { a : '1'};
a //ReferenceError: a is not defined
b //'1'
複製程式碼

物件的解構賦值,與陣列的解構賦值一樣,也可以用於巢狀結構的物件。

let a = {
    b : [
        '1',
        { c : '2' }
    ]
};

let {
    b : [
        x ,
        { c }
    ]
} = a;

x // '1'
c // '2'
b // ReferenceError: b is not defined
複製程式碼

此時b只是模式,所以無法被賦值。

預設值

物件的解構賦值也有預設值,預設值的設定方式與陣列相同,而不是依舊使用物件的內部寫法。

let { a = 1} = {};
a // 1

let { b : 1 } = {};
//SyntaxError: Invalid destructuring assignment target
複製程式碼

預設值生效的條件與陣列的解構賦值相同,屬性值必須嚴格等於undefined才會生效。

let { a = 1 } = { a : undefined };
a //1 

let { b = 1 } = { b : null };
b // null

let { c = 1 } = { c : NaN };
c // NaN
複製程式碼

在對巢狀的物件使用解構賦值時,需要注意,若子物件所在的父屬性不存在時,會報錯。這也是我在工作中,發現比較常見的一種報錯,還是需要多多注意的。特別是在使用多層結構的時候,例如res.data.id

let { a: {b} }  = { c : '1' };
TypeError: Cannot destructure property `b` of 'undefined' or 'null'
複製程式碼

在使用物件解構賦值的時候,如果要對已經宣告的變數進行解構賦值,需要小心。

let a;
{a} = {a:1};
//SyntaxError: Unexpected token =
複製程式碼

這裡是因為JavaScript引擎會將{a}當做一個程式碼塊,從而引發語法錯誤。所以需要避免將大括號寫在行首

let a;
({a} = {a:1});
a // 1
複製程式碼

由於陣列的本質是特殊的物件,因此可以對陣列進行物件屬性的解構賦值。

let a = [1, 2, 3];
let {0 : b, 2: c} = a;
b // 1
c // 3
複製程式碼

第二行程式碼的0和2代表的是陣列的位置,可以簡單理解為以下程式碼:

let a = [1,2,3];
let b =  a[0];
let c =  a[2];
複製程式碼

字串的解構賦值

字串也可以進行解構賦值,因為此時字串被轉換成了一個類似陣列的物件。

let [a, b, c, d, e] = 'hello';
a // 'h'
b // 'e'
c // 'l'
d // 'l'
e // 'o'
複製程式碼

看到這裡是不是感覺這個過程有點眼熟,其實這個過程可以理解為以下程式碼:

let x = 'hello';
a = x[0];
b = x[1];
c = x[2];
...

複製程式碼

數值和布林值的解構賦值

解構賦值時,如果等號右邊是數值或者布林值,則會先轉化成物件。

let {toString:a} = 123;
a === Number.prototype.toString // true

let {toString:a} = 123;
a === Boolean.prototype.toString // true
複製程式碼

解構賦值的規則是,若等號右邊的值不是物件或者陣列,就會先將其轉化成物件。由於undefinednull無法轉化成物件,所以對其進行解構賦值時會報錯。

let { a:b } = undefined;
//TypeError: Cannot destructure property `a` of 'undefined' or 'null'

let { a:b } = null;
//TypeError: Cannot destructure property `a` of 'undefined' or 'null'
複製程式碼

函式引數的解構賦值

function add({x,y]){
    return x+y;
}
add({1,2}); //3
複製程式碼

函式add的參數列面上為一個陣列,但是在傳入引數的那一刻,陣列引數就被解構成了2個變數,xy

函式引數的解構也可以用預設值。

function move({x=0,y=0} = {} ) {
    return [x,y];
}
move({x:1}); // [1,0];
move({}); // [0,0];
複製程式碼

函式引數的解構有另一種寫法,會得出另一種結果。

function move({x,y} = {x:0,y:0} ) {
    return [x,y];
}
move({x:1}); // [1,undefined];
move({}); // [undefined,undefined];
move(); // [0,0]
複製程式碼

上述的程式碼時為move函式引數設定預設值,而不是為解構後的xy設定預設值,所以會得出不一樣的結果。

圓括號

在使用解構賦值的時候,圓括號是否使用,是一個問題。

ES6的規則中說明,只要有可能導致解構歧義的,就不能使用圓括號。

但由於該規則的標準不容易衡量和辨別,所以一般是儘量不使用圓括號。

不能使用圓括號的情況

  1. 變數宣告語句

    let [(a)] = [1];
    let {x: (c)} = {};
    //上述兩句程式碼顯示為undefined
    
    let ({x: c}) = {};
    let {(x: c)} = {};
    let {(x): c} = {};
    let { o: ({ p: p }) } = { o: { p: 2 } };
    //上述四句程式碼會報錯。
    複製程式碼

    上述程式碼發生這種情況,主要是因為它們都是變數宣告語句,模式不能使用圓括號。

  2. 函式引數 函式引數也屬於變數宣告,因此不能帶圓括號。

    function a( [ ( b ) ] ) { return c; }
    複製程式碼
  3. 賦值語句的模式

    ( { a:b } ) = { a:1 };
    
    [ ({a:b}) , { c:d } ] = [{},{}];
    複製程式碼

    無論是將整個模式放入圓括號中,還是將部分模式放入圓括號中,都會導致報錯。

解構賦值的用途

  1. 交換變數的值

    let a = 1;
    let b = 2;
    [a,b] = [b,a]
    複製程式碼
  2. 從函式返回多個值

    通過解構賦值,可以很方便的從陣列或者物件裡獲取多個返回值。

    function arr(){
        return [1,2,3];
    }
    let [a,b,c] = arr();
    //返回一個陣列
    
    
    function arr() {
        return {
            a:1,
            b:2
        };
    }
    let { a,b } = arr();
    複製程式碼
  3. 函式引數的定義 解構賦值可以方便地將一組引數與變數名對應起來。

    // 引數是一組有次序的值
    function f([a, b, c]) { ... }f([1, 2, 3]);
    
    // 引數是一組無次序的值
    function f({a, b, c}) { ... }f({z: 3, y: 2, x: 1});
    複製程式碼
  4. 提取JSON資料 在提取JSON物件中的資料時,解構賦值能起到非常簡便和快速的作用,使得程式碼更加簡潔。

    let jsonData = {
      id: 42,
      status: "OK",
      data: [867, 5309]};
    
    let { id, status, data: number } = jsonData;
    
    console.log(id, status, number);
    複製程式碼
  5. 函式引數的預設值 通過使用解構賦值,在給函式引數賦予預設值時,整個程式碼會顯得更加簡潔。

    jQuery.ajax = function (url, {
      async = true,
      beforeSend = function () {},
      cache = true,
      complete = function () {},
      crossDomain = false,
      global = true,
      // 在這裡設定預設值
    } = {}) {
      // 這裡則是賦值的內容,若為undefined,則使用預設值
    };
    複製程式碼
  6. 遍歷Map結構 上文說過,面對擁有Iterator介面的物件時,可以使用解構賦值。在這裡,我們可以通過解構賦值快速的獲取鍵名和鍵值。

    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');
    
    for (let [key, value] of map) {
      console.log(key + " is " + value);}
    // first is hello
    // second is world
    複製程式碼
  7. 輸入模組的指定方法 載入模組時,需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。

    const { SourceMapConsumer, SourceNode } = require("source-map");
    複製程式碼

總結

在使用解構賦值的時候,整體感覺上其實就是一個遍歷過程的簡化。個人感覺最大的作用是可以將類似邏輯的程式碼進行過程簡化,從而給程式碼瘦身。

同時在其中也發現了原文章中的部分細節錯誤。例如不能使用圓括號的情況中的第一點,示例程式碼中的前兩行程式碼並沒有報錯,而是顯示undefined

然後這裡給自己留一個小作業,是在和朋友聊上述細節錯誤時發現的一個問題:為什麼let [(a)] = [1];顯示undefined,而用[(a)] = [1]則會顯示[1]

參考文章

ECMAScript 6 入門:變數的解構賦值

相關文章