ES6之解构赋值以及它的底层原理

1、基本语法

1.1、数组

// 基础类型解构
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1, 2, 3

// 对象数组解构
let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}]
console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'}

// ...解构
let [head, ...tail] = [1, 2, 3, 4]
console.log(head, tail) // 1, [2, 3, 4]

// 嵌套解构
let [a, [b], d] = [1, [2, 3], 4]
console.log(a, b, d) // 1, 2, 4

// 解构不成功为undefined
let [a, b, c] = [1]
console.log(a, b, c) // 1, undefined, undefined

// 解构默认赋值
let [x = 1] = [undefined];// x=1;
let [x = 1] = [null];// x=null; // 数组成员严格等于undefined,默认值才会生效
let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined 因为x用y做默认值时,y还没有声明

1.2 对象

// 对象属性解构
let { f1, f2 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 可以不按照顺序,这是数组解构和对象解构的区别之一
let { f2, f1 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 解构对象重命名
let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' }
console.log(rename, f2) // test1, test2

// 嵌套解构
let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } }
console.log(f11) // test11

// 默认值
let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'}
console.log(f1, rename) // current1, current2

1.3 字符串/数值/布尔值

// String
let [ a, b, c, ...rest ] = 'test123'
console.log(a, b, c, rest) // t, e, s, [ 't', '1', '2', '3' ]
let {length : len} = 'hello'; // en // 5

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

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

// Map
let [a, b] = new Map().set('f1', 'test1').set('f2', 'test2')
console.log(a, b) // [ 'f1', 'test1' ], [ 'f2', 'test2' ]

// Set
let [a, b] = new Set([1, 2, 3])
console.log(a, b) // 1, 2

2、使用场景

2.1、 浅拷贝

let colors = [ "red", "green", "blue" ];
let [ ...clonedColors ] = colors;
console.log(clonedColors);  // "[red,green,blue]"
注意这里是浅拷贝

2.2、 交换变量

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

2.3、遍历Map结构

var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
    console.log(key + " is " + value);
}

2.4、函数参数

function add(a,b,{c,d}){
    // ...
}
add(1,2,{});
add(1,2)//Uncaught TypeError: Cannot destructure property `c` of 'undefined' or 'null'
解构赋值的规则是,若等号右边的值不是对象或者数组,就会先将其转化成对象。由于undefined和null无法转化成对象,所以对其进行解构赋值时会报错。

它实际上是这样运行的:

function add(a, b, options) {
    let { c,d } = options;
    // ...
}

由于add(1,2)没有传options导致异常,如果我们options是选填的那么可以像下面这样

function add(a,b,{c,d}={}){
    // ...
}

当然大部分情况我们可以给默认值

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

上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。 undefined就会触发函数参数的默认值。

[1, undefined, 3].map((x = 'yes') => x);

3、基本原理

解构是ES6提供的语法糖,其实内在是针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值。这里需要提前懂得ES6的两个概念: - Iterator - 可迭代对象

3.1、Iterator概念

Iterator是一种接口,为各种不一样的数据解构提供统一的访问机制。任何数据解构只要有Iterator接口,就能通过遍历操作,依次按顺序处理数据结构内所有成员。ES6中的for of的语法相当于遍历器,会在遍历数据结构时,自动寻找Iterator接口。 Iterator作用: - 为各种数据解构提供统一的访问接口 - 使得数据解构能按次序排列处理 - 可以使用ES6最新命令 for of进行遍历

function makeIterator(array) {
    var nextIndex = 0
    return {
      next: function() {
        return nextIndex < array.length ?
            {value: array[nextIndex++]} :
            {done: true}
        }
    };
  }

var it = makeIterator([0, 1, 2])
console.log(it.next().value) // 0
console.log(it.next().value) // 1
console.log(it.next().value) // 2

3.2、可迭代对象

可迭代对象是Iterator接口的实现。这是ECMAScript 2015的补充,它不是内置或语法,而仅仅是协议。任何遵循该协议点对象都能成为可迭代对象。可迭代对象得有两个协议:可迭代协议迭代器协议

可迭代协议:对象必须实现@@iterator方法。即对象或其原型链上必须有一个名叫Symbol.iterator的属性。该属性的值为无参函数,函数返回迭代器协议。

ES6之解构赋值以及它的底层原理

迭代器协议:定义了标准的方式来产生一个有限或无限序列值。其要求必须实现一个next()方法,该方法返回对象有done(boolean)value属性。

ES6之解构赋值以及它的底层原理

通过以上可知,自定义数据结构,只要拥有Iterator接口,并将其部署到自己的Symbol.iterator属性上,就可以成为可迭代对象,能被for of循环遍历。

3.3、解构语法糖

StringArrayMapSet等原生数据结构都是可迭代对象,可以通过for of循环遍历它。故可以通过ES6解构语法糖依次获取对应的值。

// String
let str = 'test'
let iterFun = str[Symbol.iterator]
let iterator = str[Symbol.iterator]()
let first = iterator.next() // 等效于 let [first] = 'test'
console.log(iterFun, iterator, first)
// 打印
// [Function: [Symbol.iterator]], {}, { value: 't', done: false }

// Array
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

// 以下等效于 let [first, second, third, four] = ['a', 'b', 'c']
let first = iter.next() // { value: 'a', done: false }
let second = iter.next() // { value: 'b', done: false }
let third = iter.next() // { value: 'c', done: false }
let four = iter.next() // { value: undefined, done: true }

4、性能

当我们有解构赋值的形式来做函数参数时,执行的时候会增加很多中间变量,内存也会比之前高。但是业务代码还是更加关注可读性和可维护性。如果你写的是库代码,可以尝试这种优化,把参数展开后直接传递,到底能带来多少性能收益还得看最终的基准测试。

上一篇:洛谷 P4437 [HNOI/AHOI2018]排列(贪心+堆,思维题)


下一篇:JavaScript中async await的基本用法