JS解構賦值

2022-03-15 19:00:15
在日常開發中,我們經常會定義很多的陣列或者物件,然後從陣列或物件中提取出相關的資訊。在傳統的 ES5 及以前的版本中,我們通常會採用下面的方法獲取陣列或者物件中的值。
var arr = ['one', 'two', 'three'];
var one = arr[0];
var two = arr[1];

var obj = {name: 'kingx', age: 21};
var name = obj.name;
var age = obj.age;
我們會發現,如果陣列選項或者物件屬性值很多,那麼在提取對應值的時候會寫很多冗餘的程式碼。有沒有什麼方法能減少冗餘程式碼的編寫嗎?

ES6 增加了可以簡化這種操作的新特性——解構,它可以將值從陣列或者物件中提取出來,並賦值到不同的變數中。

我們主要從兩方面進行講解,一方面是陣列的解構賦值,另一方面是物件的解構賦值。

陣列的解構賦值

針對陣列,在解構賦值時,使用的是模式匹配,只要等號兩邊陣列的模式相同,右邊陣列的值就會相應賦給左邊陣列的變數。
let [arg1, arg2] = [12, 34];
console.log(num1); // 12
console.log(num2); // 34
在上面的程式碼中,我們從陣列中解構了“12”和“34”這兩個值,並分別賦給 arg1 和 arg2 這兩個變數。

我們還可以只解構出感興趣的值,對於不感興趣的值使用逗號作為預留位置,而不指定變數名。
let [, , num3] = [12, 34, 56];
console.log(num3); // 56
當右邊的陣列的值不足以將左邊陣列的值全部賦值時,會解構失敗,對應的值就等於“undefined”。
let [num1, num2, num3] = [12, 34];
console.log(num2); // 34
console.log(num3); // undefined
陣列的解構賦值有很多的使用場景,可以提高開發時的效率,接下來將一一講解。

陣列解構預設值

在陣列解構時設定預設值,可以防止出現解構得到 undefined 值的情況。具體的做法是在左側的陣列中,直接給變數賦初始值。
let [num1 = 1, num2] = [, 34];
console.log(num1); // 1
console.log(num2); // 34
需要注意的是,ES6 在判斷解構是否會得到 undefined 值時,使用的是嚴格等於(===)。只有在嚴格等於 undefined 的情況下,才會判斷該值解構為 undefined,相應變數的預設值才會生效。
let [
    num1 = 1,
    num2 = 2,
       num3 = 3
] = [null, ''];

console.log(num1);  // null
console.log(num2);  // ''
console.log(num3);  // 3
在上面的範例中,變數 num1 會被解構為 null,而不是 undefined,null 在判斷嚴格等於時,並不等於 undefined,因此預設值不會生效。
  • 變數 num2 會解構為空字串,也不是 undefined,預設值也不會生效。
  • 變數 num3 在右側陣列中,並沒有對應的值,因此會解構為 undefined,預設值生效,num3 值為“3”。

交換變數

在使用解構賦值以前,當我們需要交換兩個變數時,需要使用一個臨時變數,以下是一個經典的寫法。
var a = 1;
var b = 2;
var tmp; // 臨時變數

tmp = a;
a = b;
b = tmp;

console.log(a);  // 2
console.log(b);  // 1
如果使用陣列的解構賦值,交換變數的操作將會變得很簡單,只需要在等式兩邊的陣列中交換兩個變數的順序即可。
var a = 1;
var b = 2;
// 使用陣列的解構賦值交換變數
[b, a] = [a, b];

console.log(a);  // 2
console.log(b);  // 1

解析函數返回的陣列

函數返回陣列是一個很常見的場景,在獲取陣列後,我們經常會提取陣列中的元素進行後續處理。

如果使用陣列的解構賦值,我們可以快速地獲取陣列元素值。
function fn() {
    return [12, 34];
}

let [num1, num2] = fn();

console.log(num1); // 12
console.log(num2); // 34

巢狀陣列的解構

在遇到巢狀陣列時,即陣列中的元素仍然是一個陣列,解構的過程會一層層深入,直到左側陣列中的各個變數均已得到確定的值。
let [num1, num2, [num3]] = [12, [34, 56], [78, 89]];

console.log(num1); // 12
console.log(num2); // [34, 56]
console.log(num3); // 78
在上面的範例中,num2 對應的位置是一個陣列,得到的是“[34, 56]”;[num3] 得到的是一個陣列“[78, 89]”,解構並未完成,對於 num3 會繼續進行解構,最後得到的是陣列第一個值“78”。

函數引數解構

當函數的引數為陣列型別時,可以將實參和形參進行解構。
function foo([arg1, arg2]) {
    console.log(arg1); // 2
    console.log(arg2); // 3
}
foo([2, 3]);
上述範例中,foo() 函數的實參為 [2, 3],形參為 [arg1, arg2],使用陣列的解構賦值時,得到變數 arg1 的值為“2”,變數 arg2 的值為“3”。

物件的解構賦值

在 ES6 中,物件同樣可以進行解構賦值。陣列的解構賦值是基於陣列元素的索引,只要左右兩側的陣列元素索引相同,便可以進行解構賦值。但是在物件中,屬性是沒有順序的,這就要求右側解構物件的屬性名和左側定義物件的變數名必須相同,這樣才可以進行解構。

同樣,未匹配到的變數名在解構時會賦值“undefined”。
let {m, n, o} = {m: 'kingx', n: 12};
console.log(m); // kingx
console.log(n); // 12
console.log(o); // undefined
當解構物件的屬性名和定義的變數名不同時,必須嚴格按照 key: value 的形式補充左側物件。
let {m: name, n: age} = {m: 'kingx', n: 12};
console.log(name); // kingx
console.log(age); // 12
而當 key 和 value 值相同時,對於 value 的省略實際上是一種簡寫方案。
let {m: m, n: n} = {m: 'kingx', n: 12};
// 簡寫方案
let {m, n} = {m: 'kingx', n: 12};
事實上,物件解構賦值的原理是:先找到左右兩側相同的屬性名(key),然後再賦給對應的變數(value),真正被賦值的是 value 部分,並不是 key 的部分。

在如下所示的程式碼中,m 作為 key,只是用於匹配兩側的屬性名是否相同,而真正被賦值的是右側的 name 變數,最終 name 變數會被賦值為“kingx”,而 m 不會被賦值。
let {m: name} = {m: 'kingx'};
console.log(name);// kingx
console.log(m); // ReferenceError: m is not defined
和陣列的解構賦值一樣,物件的解構賦值也有很多的使用場景,接下來將一一講解。

物件解構的預設值

物件解構時同樣可以設定預設值,預設值生效的條件是對應的屬性值嚴格等於 undefined。
let {m, n = 1, o = true} = {m: ‘kingx’, o: null};
console.log(m); // kingx
console.log(n); // 1
console.log(o); // null,因為null與undefined不嚴格相等,預設值並未生效
當屬性名和變數名不相同時,預設值是賦給變數的。
let {m, n: age = 1} = {m: 'kingx'};
console.log(m);   // kingx
console.log(age); // 1
console.log(n);   // ReferenceError: n is not defined

巢狀物件的解構

巢狀的物件同樣可以進行解構,解構時從最外層物件向內部逐層進行,每一層物件值都遵循相同的解構規則。
let obj = {
    p: [
        'Hello',
        {y: 'World'}
    ]
};
let {p: [x, {y: name}]} = obj;
console.log(x); // Hello
console.log(name); // World
console.log(y); // ReferenceError: y is not defined
在上面的範例中,變數 obj 是一個巢狀物件,會存在多次解構的過程。

第一次解構從最外層的屬性 p 開始,屬性 p 對應的值為一個陣列[‘Hello’, {y: ‘World’}],對應左側的[x, {y: name}]。

第二次解構得到的 x 值為“Hello”,{y: ‘World’}對應左側的{y: name} 。

第三次解構得到 name 值為“World”,解構結束。

而 y 僅僅作為匹配屬性名的 key,不會參與賦值,因此輸出 y 值時,會丟擲異常。
注意:當父層物件對應的屬性不存在,而解構子層物件時,會出錯並丟擲異常。
let obj = {
    m: {
        n: 'kingx'
    }
};

let {o: {n}} = obj;
console.log(n); //TypeError: Cannot match against 'undefined' or 'null'.
從丟擲的異常資訊中可以看出,是因為無法匹配到 undefined 或者 null 的屬性才造成異常。

因為在 obj 物件中,外層的屬性名是 m,而在左側的物件中,外層屬性名是 o,兩者並不匹配,所以 o 會解構得到“undefined”。而對 undefined 再次解構想要獲取 n 屬性時,相當於呼叫 undefined.n,會丟擲異常。

選擇性解構物件的屬性

假如一個物件有很多通用的函數,在某次處理中,我們只想使用其中的幾個函數,那麼可以使用解構賦值。
let { min, max } = Math;
console.log(min(1, 3));  // 1
console.log(max(1, 3));  // 3
在上面的範例中,我們只想使用 Math 物件的 min() 函數和 max() 函數,min 變數和 max 變數解構後的值就是 Math.min() 函數和 Math.max() 函數,在後面的程式碼中可以直接使用。

函數引數解構

當函數的引數是一個複雜的物件型別時,我們可以通過解構去獲得想要獲取的值並賦給變數。
function whois({displayName: displayName, fullName: {firstName: name}}){
    console.log(displayName + "is" + name);
}
const user = {
    id: 42,
    displayName: "jdoe",
    fullName: {
        firstName: "John",
        lastName: "Doe"
    }
};
whois(user); // jdoe is John
在上面的範例中,whois() 函數接收的引數是一個複雜的物件型別,可以通過巢狀的物件解構得到我們想要的 displayName 屬性和 name 屬性。