运算符之赋值运算符

264 阅读6分钟

赋值运算符之解构赋值

在ES5之前,我们在对对象进行赋值的时候,都是通过 = 等于运算符的,ES6提供了一种新的赋值方式,这种解构赋值可以通过“模式匹配”,只要两侧的模式结构相同,那么就会按照位置顺序,对变量赋值。

这种赋值的适用场景是要对多个变量进行同时赋值、交换变量的值、获取从函数返回的多个值、函数参数的定义、提取json数据等等。

数组解构赋值

适用场景:只要数据结构具有Iterator接口就可以使用数组的解构赋值。

基本使用:同时为多个变量赋值;

let [x, y, z] = [1, 2, 3];
//等价于
let x = 1;
let y = 2;
let z = 3;

如果两侧的模式结构一摸一样,那就会解构成功,但是如果两侧结构存在偏差,那么就会存在解构失败或者解构不完全的情况。

  • 解构失败

    let [a, b, ...rest] = ['a'];
    //输出结果: a undefined []
    ​
    let [x, , y] = [1, 2];
    /*
    输出结果:1 undefined
    (为什么是undefined,就是因为赋值解构的原则是根据两侧的模式结构对应赋值的,
    那么x对应的是1,第二个空的变量对应的是2,第三个变量y对应的是undefined)
    */
    ​
    

    从上述结果我们可以看到如果解构失败,那么对应的变量就会被赋值undefined,这里rest被赋值[],Rest 属性收集那些尚未被解构模式拾取的剩余可枚举属性键。在数组的解构赋值中,rest参数用于接收剩余的值,存储在数组中,所以如果解构失败对应返回的是空数组。

    如果使用默认值,那我们就可以避免这种解构失败的情况: 解构赋值是允许设置默认值,只有当等式右边对应的值严格等于undefined,默认值才会生效。

    let [q = 1, w = 2, e] = [3, , 3]
    //3 2 3
    ​
    let [x=1, y=x] = [undefined,null];
    //这里输出 1, null
    /*
        首先明确的是默认值是可以引用赋值的,也就是说因为等式左边已经先将x赋值1了,
        所以y是可以引用x的
        但是如果是[x=y, y=1],这种就不可以,因为当进行解构的时候,y还没声明,
        这时候如果直接将y赋值给x,那么就会报错说y还没有定义。
        所以在这种情况下,如果等式右边都是undefined的话,那么x=1,y=x=1,
        但是我们现在看到的是y对应右边的结构中,
        它的值是null,null === undefined是false的,
        所以这里是输出1,null
    */
    

    注意: 解构赋值的默认值是惰性赋值的,也就是说只要在等式右边的属性值是严格等于undefined的情况下,默认值才会被赋值到变量上面。也就是以下的原理

    //对于 let[x=1] = [2],其底层原理是:
    let x;
    if([2][0]===undefined){
        x = 1;
    }else{
        x = [2][0]
    }
    

    还有一种解构失败的情况:就是当等式右边并不是iterator结构。

    image-20220106120659248.png

  • 解构不完全

    这种情况是左边的模式只匹配一部分右边的数组。具体实例如下

    let [x, y] = [1, 2, 3];
    //输出结果: 1 2
    

对象解构赋值

对象的解构赋值规则和数组的解构赋值规则差别不大,由于数组的特殊性,它在进行解构赋值的时候时候按照顺序来的,那么对象的解构赋值是根据属性名来匹配的

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
//输出: bar: bbb; foo: aaa

正是因为对象的解构赋值是根据属性名来的,所以一般我们只研究其解构失败的情况

  • 解构失败

    对象的解构失败会将对应的变量名设置为undefined

    let { bar, foo } = { foo: 'aaa' }
    //输出: foo: aaa;   bar: undefined
    

    这里首先要区分一下【属性名】和【变量名】在对象解构中的区别: 对象解构的过程中,模式匹配是先根据寻找等式的右边是否有相同的属性名,找到相同的属性名就将对应的属性值赋值给等式左边的变量名。具体看以下实例

    let {foo: baz} = {foo: 'aaa'}
    //ReferenceError: foo is not defined
    /*
        这里foo是属性名,baz才是变量名
        执行的流程:
        1. 查找等式右边的foo属性名
        2. 找到了右边的foo,将foo的值aaa赋值给左边的baz变量名
        
        由上述可知,我们平时写的let {foo} = {foo: 'aaa'}
        其实是等式let {foo: foo} = {foo: 'aaa'}的缩写
    */
    

    同样的,针对解构失败,我们也可以为变量设置默认值,只有在右边等式对应的属性值严格等于undefined,默认值才会生效,作用机理同样是惰性的

    let { x: y = 5 } = { x: 1 }
    //这里输出的y = 1
    /*
        对于左侧的结构,是这样的:
        x是属性名,y是变量名,5是y的默认值
        因为右侧的x是1,不是undefined
        所以这里左侧的默认值并没有生效,所以y的值是1
    */
    let { x: y = 5 } = {}
    //y = 5
    

    对象解构的时候是可以查找原型链的,也就是说对应等式右侧的模式中,如果找不到对应的属性名,那么是会去它的原型对象中寻找的。

    还需要注意的是,我们尽量不要在解构赋值的过程中使用圆括号,除了在对于已经声明好的变量进行解构赋值的时候,如下面的情况:

    
    let x;
    {x} = {x:1};
    //这样写是错误的,因为这样JavaScript引擎就会把{x}当成代码块来处理,而不是解构赋值
    ({x} = {x:1});
    //这样写才是对的,但是要注意把let x后面加一个分号,不然连在一起的话还是会把它当作一个方法,而不是解构赋值
    

函数解构赋值

在普通函数中,如果我们给函数定义了形参,这个过程就是在声明变量,传入实参的过程就是给刚刚声明的形参赋值,那么如果我们没有传入实参,对应的形参将会被赋值undefined;

那么当我们引入解构赋值就可以给函数的形参设置默认值。

注意以下几种函数参数解构的形式:

  1. 为变量x,y指定默认值,但是不是为函数的参数指定默认值

    function move({ x = 1, y = 1 }) {
         console.log(x, y);
    }
    move({});
    move({ x: 10, y: 100 });
    move();     //这里会报错,因为相当于{ x = 1, y = 1 } = undefined,解构的模式不对
    
  2. 为函数的参数指定默认值,也为x,y指定默认值

    function move({ x = 1, y = 1 } = {}) {
        console.log(x, y);
    }
    move({});
    move();     
    //上述两个方法的调用是一样的
    move({ x: 10, y: 100 });
    
  1. 为函数的参数指定默认值,不为x,y指定默认值

    function move({x, y} = {x: 10, y: 100}){
        console.log(x, y);
    }
    move({});       //undefined undefined
    move();     
    move({ x: 11, y: 110 });
    

当然还有字符串、布尔值的解构赋值等,有兴趣的可以去了解一下~