阅读 596

ES6系列入门学习记录:变量的解构赋值

前言

年都过去半个月了,我终于又重新开始更新了。虽然只是第二篇,但是我会继续加油努力,一定不会放弃更新的。在文章中若有什么不妥或者您有更多建议的话,欢迎和期待您给我留言,您的每一个留言都可能成为我进步的助力,十分感谢。那就废话不多说直接开始吧。

概念

解构赋值,顾名思义,就是解开一个结构,然后从里面拿出值用来给变量赋值赋值。所以解构赋值主要是以数据类型来划分的。

数组的解构赋值

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 入门:变量的解构赋值

关注下面的标签,发现更多相似文章
评论