解构赋值的一些细节

Clloz · · 105次浏览 ·

前言

解构赋值 Destructuring assignmentES6 提供的新语法,通过解构赋值我们可以从对象或数组(类数组对象也可)中取出属性或值,赋值给其他变量。本文整理一下比较容易忽略和不太好理解的点。

知识点

undefined 的确定

ES6 用严格相等运算符来判断一个位置是否有值。在解构赋值中只有一个位置的值严格等于 undefined,我们设置的默认值才会生效。

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

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

默认值表达式惰性求值

如果解构赋值中某个变量的默认值是个表达式,那么这个表达式是惰性求值的,也就是只有需要执行的时候才会求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1]; //f() 不会执行

对象解构机制

数组的解构赋值是根据变量的位置来确定其值的。由于对象不像数组一样是按次序排列的,所以对象的解构赋值只能根据变量的名称到对象中查找。但是需要注意的是,我们要区分好用于匹配的模式(可以理解为键值对中的键)和具体的对象,特别是在嵌套的对象解构中。我的理解就是解构表达式中的变量表示中不管嵌套多少层,有多少标识符,第一个无法在对象中找到的标识符就是变量的名称,如果每一个标识符都能找到,那么就是隐藏了一个和最后一个标识符同名的变量。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
//foo是匹配的模式,baz才是变量,模式只是用来到对象中查找属性,而变量则是最后赋值的目标

let {foo: {bar}} = {baz: 'baz'}; //foo 无法在对象中找到,所以是 undefined,此时再想向下找属性就会报错

//对象的解构赋值可以找原型上的属性
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo // "bar"

//如果解构赋值语句不是变量声明语句(前面没有var,let,const),即对已经声明的变量进行进行解构赋值需要注意加上括号
let x;
{x} = {x: 1}; //Uncaught SyntaxError: Unexpected token '='  行首的大括号会被引擎认为是代码块
({x} = {x: 1}); //这是正确写法,但是和立即执行函数一样,该语句的前面一行最好加上分号,否则可能会被当做函数调用。

//数组是特殊的对象,所以可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

数组的解构赋值

数组的解构赋值表达式的右值不是一个可遍历结构,则会报错。

// 报错 Uncaught TypeError: xxx is not iterable
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

数组的解构赋值也可以这样使用 let [,m] = [2,3]

数值、字符串和布尔型

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象,字符串被转为类数组对象,数值和布尔型则转为包装对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值,都会报错。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

//字符串转为的对象有length属性
let {length : len} = 'hello';
len // 5

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

let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

函数参数的解构赋值需要注意的是默认参数设定的机制。

//设置解构赋值的默认值
function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
move({x: undefined, y: undefined}) //[0, 0]

//设置参数默认值
function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
move({x: undefined, y: undefined}) //[undefined, undefined]

//注意下面这种会直接报错
function move({x = 0, y = 0}) {
  return [x, y];
}
move() //Uncaught TypeError: Cannot read property 'x' of undefined

我们需要记得的一个前提是,解构赋值表达式中的默认值要生效的条件就是对应的数组元素或者对象属性严格等于undefined,这一点对于我们理解函数中解构赋值的写法非常重要。另外一点就是我们要明白默认值的意义,默认值就是当我们在函数调用时没有接收到这个值或者接收的值为 undefined,就使用默认值。

我们可以在上面的两端代码中,我们得到了截然不同的结果,第一段代码中的函数参数部分为 {x = 0, y = 0} = {},这是一个解构赋值表达式,因为右值为空对象,所以 xy 对应的对象属性都是严格等于 undefined,也就是我们为 xy 成功设置了默认值。后面我们看到当函数调用时,如果没有传入参数或者传入的参数是 undefined,输出的结果就是默认值。

而在第二段中,我们的函数参数部分为 {x, y} = { x: 0, y: 0 },这也是一个解构赋值表达式。但是需要注意的是,这个表达式不满足我们上面写的对应的数组元素或者对象属性严格等于undefined,所以 xy 是没有默认值的。我们也可以看到后面的函数调用,当我们没有传入参数或者传入的参数为 undefined 的时候,输出就是 undefined。这里其实我们完全可以吧函数理解为 function move(x = 0, y = 0)(/** code **/)(当然还是能够接受参数进行解构赋值的),这就很明显了,我们的解构赋值只是给了函数一个默认参数,所以最后执行 move() 的时候输出了 [0, 0]

函数默认参数的使用也和解构赋值的默认值的生效类似,当传入的参数严格等于 undefined 即使用默认参数,[1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ]

上面的函数用的是对象作为参数,其实数组作为参数的情形也是一样的。但是注意数组解构赋值的右值必须是一个可遍历结构。

//设置默认参数
function a([x, y] = [5, 6]){console.log(x,  y)}
a() //5, 6
a([1]) //1, undefined
a('asdf') // a, s
a(1) //VM1241:1 Uncaught TypeError: undefined is not a function(不知道为什么不是not iterable的报错)

//设置默认值
function a([x=5, y=6] = []){console.log(x,  y)}
a() // 5, 6
a([1]) //1, 6
a('asdf') // a, s
a(1) //VM1241:1 Uncaught TypeError: undefined is not a function

如果你希望能够在不提供任何参数的情况下调用该函数,就使用默认值模式。如果你只是想用解构赋值给函数一个默认参数则使用另一种。

解构赋值中的括号

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

总结起来就是两个规则:
1. 变量声明语句中不可以使用(函数参数也属于变量声明)
2. 不可以把模式(键)包含在小括号中(数组的模式是按位置匹配,所以把数组元素括起来可以)

// 变量声明 报错
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

//函数参数 报错
function f([(z)]) { return z; }
function f([z,(x)]) { return x; }

//模式括号 报错
({ p: a }) = { p: 42 };
([a]) = [5];

[({ p: a }), { x: c }] = [{}, {}];

//正确
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

除了大括号在行首用括号将表达式括起来,其他情况尽量不要使用

用途

  1. 将对象的方法赋值给某个变量
    // 例一
    let { log, sin, cos } = Math;
    
    // 例二
    const { log } = console;
    log('hello') // hello
    
  2. 交换变量的值
    let x = 1;
    let y = 2;
    
    [x, y] = [y, x];
    
  3. 函数返回多个值
    // 返回一个数组
    
    function example() {
      return [1, 2, 3];
    }
    let [a, b, c] = example();
    
    // 返回一个对象
    
    function example() {
      return {
        foo: 1,
        bar: 2
      };
    }
    let { foo, bar } = example();
    
  4. 函数参数定义
    // 参数是一组有次序的值
    function f([x, y, z]) { ... }
    f([1, 2, 3]);
    
    // 参数是一组无次序的值
    function f({x, y, z}) { ... }
    f({z: 3, y: 2, x: 1});
    
  5. 提取 JSON 数据
    let jsonData = {
      id: 42,
      status: "OK",
      data: [867, 5309]
    };
    
    let { id, status, data: number } = jsonData;
    
    console.log(id, status, number);
    // 42, "OK", [867, 5309]
    
  6. 函数参数默认值
    jQuery.ajax = function (url, {
      async = true,
      beforeSend = function () {},
      cache = true,
      complete = function () {},
      crossDomain = false,
      global = true,
      // ... more config
    } = {}) {
      // ... do stuff
    };
    
  7. 遍历 Map 解构
    jQuery.ajax = function (url, {
      async = true,
      beforeSend = function () {},
      cache = true,
      complete = function () {},
      crossDomain = false,
      global = true,
      // ... more config
    } = {}) {
      // ... do stuff
    };
    
    // 获取键名
    for (let [key] of map) {
      // ...
    }
    
    // 获取键值
    for (let [,value] of map) {
      // ...
    }
    
  8. 输入模块的指定方法
    const { SourceMapConsumer, SourceNode } = require("source-map");
    

参考文章

  1. 《ECMAScript6入门》 —— 阮一峰
  2. MDN

Clloz

人生をやり直す

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*

 

EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00