ES6+ 新特性 —— 解构赋值

240 阅读6分钟

一、写在前面

笔者是一名本科大二前端小白,在此留下前端学习过程中值得记录的一切。
在这个系列中,笔者整理了在学习ES6+过程中遇到的实用新特性。
内容参考了W3C、菜鸟等文档教程,也有笔者对初次学习该知识点时笔记的总结。

本期带大家了解解构赋值的内容,并熟悉它的语法和使用场景。
由于是笔者的第一篇文章,如果有瑕疵、错误,欢迎大家在评论区指出、交流!

二、解构赋值是什么?

是对赋值运算符的扩展,对数组/对象进行模式匹配,然后对其中的变量进行赋值。
就是一种打破数据结构,将其拆分为更小部分的过程。
如此就能让代码更加简洁易读,语义更加清晰明了。

三、为什么需要它?

假设我们有一个数据对象,其中还内嵌了两级对象以存储更加具有层次的数据:

const trainee = {
    name: 'cxk',
    age: 24,
    skills: {
        sing: {
            song: 'jntm'
        }
        jump: 99,
        rap: 98
    }
}
function showSkill(trainee){
    console.log('名:' + trainee.name);
    console.log('唱:' + trainee.skills.sing.song);
    console.log('rap:' + trainee.skills.rap);
}

某些属性由于还具有多级子对象,访问的深度加大,这意味着在写代码时需要更多的输入。

但是通过解构赋值,我们可以用更简洁的语法、更有表现力的写法来实现一样的效果。

四、语法

1. 数组

ES5中为变量赋值,只能直接一个个地指定值。

let c = 1,
    x = 2,
    k = 3;

解构赋值却允许这样写:

let [c, x, k] = [1, 2, 3];  // 可以从数组中提取值,按照对应位置,对变量赋值。

这种写法属于模式匹配,只要等号两边的模式相同,左边变量就会被赋予右边对应的值。

如果等号右边不是可遍历的结构(包括数、布尔值、nullundefined),那就会报错。

let [a] = 0;
let [a] = true;
let [a] = undefined;
let [a] = {};          // 都报错!

为什么这些数据类型不能被遍历呢?其实在解构时默认会去调用Iterator接口,如果该数据类型没有Iterator接口,就不可被遍历!

在这里笔者有必要介绍一下Iterator接口:

是一种迭代器,为各种不同的数据结构提供统一的访问机制,主要表现为遍历操作。
任何数据类型只要部署了Iterator接口,就可以被遍历(迭代)。
ES6中只要一个数据类型具有Symbol.iterator属性,那么就认为是可迭代的。
包括ArrayMapSetargumentsString
字符串可以解构赋值是因为此时被转换成了一个类似数组的对象。

以下代码简单实现Iterator接口:

let cxk = [1, 2, 3];
// 依次处理数据类型里的每个成员
function Iterator(cxk) {
    let index = 0;
    return {
        next: () => {
            if (index < cxk.length) {
                return {
                    value: cxk[index++],
                    isDone: false
                }
            } else {
                return {
                    value: undefined,
                    isDone: true
                }
            }
        }
    }
}
let trainee = cxk[Symbol.iterator]();
console.log(trainee.next());
console.log(trainee.next());
console.log(trainee.next());
console.log(trainee.next());

打印结果如下,可以看到完成了对数组cxk内部成员的遍历操作: image.png 好了,介绍完Interator接口。我们回到解构赋值。

以下这样写都是可以的:

// 多级嵌套:
let [c, [[x], k]] = [1, [[2], 3]];  // c = 1, x = 2, k = 3

// 省略某些元素,只为需要的元素提供变量名:
let [a, , b] = [1, 2, 3];           // a = 1, b = 3

// 不完全解构:
let [x, y] = [1, 2, 3];             // x = 1, y = 2
let [a, [b], c] = [1, [2, 3], 4];   // a = 1, b = 2, c = 4

// 搭配剩余运算符:
let [a, ...b] = [1, 2, 3, 4];       // a = 1, b = [2, 3, 4]
let [x, y, ...z] = ['a'];           // x = 'a', y = undefined, z = []

// 所有可遍历对象皆可进行解构赋值:
let [x, y, z] = 'cxk';              // x = 'c', y = 'x', z = 'k'
let [w, v, z] = new Set(['c', 'x', 'k']);  // w = 'c', v = 'x', z = 'k'

2. 对象

在赋值操作符左边放置一个对象字面量,没有顺序要求

可以属性名 = 变量名

let {name, age} = {name: 'cxk', age: 24};  // name = 'cxk', age = 2

也可属性名 ≠ 变量名

let {name: fullName, age: realAge} = {name: 'fpb', age: 24};  // fullName = 'fpb', realAge = 20

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量
真正被赋值的是后者,而非前者。

let {name: realName} = {name: 'cxk', age: 24};   // realName = 'cxk'
// name: error: name is not defined (name相当于匹配的模式,不能被赋值)

以下这样写都是可以的:

// 多级嵌套:
let obj = {
    p: [
        '大家好',
    	{y: 'cxk'}
    ]
};                               // x = '大家好', y = 'cxk' 
let { p: [x, {y}] } = obj;       // 注意,这时 p 是"模式",不是变量,因此不会被赋值
                                 
// 省略某些元素,只为需要的元素提供变量名:
let obj = {
    p: [
        '大家好', 
        {y: 'cxk'}
    ] 
};
let { p: [x, { }] } = obj;       // x = '大家好'

// 不完全解构:
let obj = { p: [{y: 'cxk'}] };
let { p: [{y}, x ] } = obj;      // x = undefined, y = 'cxk'

// 搭配剩余运算符:
let {x, y, ...rest} = {a: 1, b: 2, c: 3, d: 4};    
// x = 1, y = 2, rest = {c: 3, d: 4}

3. 默认值

(1) 数组

  • 在解构赋值表达式中也可以为数组的任意位置添加默认值。

    当指定位置的属性不存在,或者其值为undefined时使用默认值。
let [a = true] = [undefined];  // a = true      
let [x, y = 'b'] = ['a'];      // x = 'a', y = 'b'

当解构模式有匹配结果,且匹配结果是undefined时,会触发默认值作为返回结果。

ES6 内部使用 === 判断一个位置是否有值。所以只有当一个数组成员严格等于undefined,默认值才会生效。

let [x = 3, y = x] = [];     // x = 3, y = 3  x 与 y 匹配结果为 undefined,触发默认值:x = 3; y = x = 3
let [x = 3, y = x] = [1];    // x = 1, y = 1  x 正常解构,y 匹配结果undefined ,触发默认值:y = x = 1
let [x = 3, y = x] = [1, 2]; // x = 1, y = 2  x 与 y 正常解构赋值,匹配结果:x = 1,y = 2

注意nullundefined不严格相等。

let [x = 1] = [undefined];  // x = 1
let [x = 1] = [null];       // x = null

如果默认值是一个表达式,那么这个表达式是惰性求值的(只有在用到时,才会求值)

function f() {
    return 'cxk';
}
let [x = f()] = ['fpb'];   // x 能取到值 fpb,所以函数 f 不会执行

(2) 对象

语法大多同数组:

let {x = 3} = { };             // x = 3
let {x, y = 5} = {x: 1};       // x = 1, y = 5
let {x: y = 3} = { };          // y = 3

默认值生效的条件依然是属性值严格等于undefined

let {x = 3} = {x: undefined};  // x = 3
let {x = 3} = {x: null};       // x = null

4. 函数参数

下面代码中,函数getMul的参数表面上是一个数组,但在传入参数时,数组参数就被解构成变量xy。对于函数内部来说,它能感受到的参数就是xy

function getMul([x, y]) {
    return x * y;
}
getMul([2, 3]);   // 6 

也可使用默认值:

function f({x = 4, y = 5} = { }) {    // 解构失败即使用默认值
    return [x, y];
}
f({x: 1, y: 2});  // [1, 2]
f({x: 3});        // [3, 5]
f();              // [4, 5]

五、使用场景

1. 交换参数的值

let x = 1,
    y = 2;
[x, y] = [y, x];

2. 从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。

使用解构赋值,取出这些值就非常方便。

// 返回数组
function arr() {
    return [1, 2, 3];
}
let [a, b, c] = arr();
// 返回对象
function obj() {
    return {
    	a: 1,
    	b: 2
    };
}
let {x, y} = obj();

3. 提取Json数据

对获取Json数据中的特定数据尤其方便。

let jsonData = {
    id: 142857,
    status: 'OK',
    data: [1234, 5678]
};
let {id, status, data: number} = jsonData;  
// id = 142857, status = 'OK', number = [1234, 5678]

可以混合解构:

let pra = {
    name: 'cxk',
    fund: {
    	start:{
            num: 100,
            isMan: true
        }
    },
    index: [0,3]
};

let {
    fund: {start:{num}},
    index: [start]
} = pra;

console.log(num);         // 100
console.log(start);       // 0

4. 可以方便地遍历Map等数据结构

使用for...of可以遍历具有Iterator接口的数据对象。

Map结构具有Iterator接口,配合变量的解构赋值,获取键名和键值就非常方便。

let map = new Map();
map.set('1th', 'fpb');
map.set('2rd', 'cxk');
for (let [key, value] of map) {
    console.log(key + ": " + value);    // 1th: fpb
}                                       // 2rd: cxk