Destructuring、Rest 与 Spread 运算


ES6 支持 Destructuring 的语法,它的概念像是模式比对(Pattern match)(然而不完全是),当你将某个结构拆解并分别指定给变量时,经常出现某种模式时,就可以使用这类语法。例如:

var scores = [80, 90, 99];
var score0 = scores[0];
var score1 = scores[1];
var score2 = scores[2];

scores的结果,可能来自某个函数返回值,像这样的例子,在 ES6 中可以写成:

let scores = [80, 90, 99];
let [score0, score1, score2] = scores;

在这个例子中使用数组,该说它是数组解构(Array destructing)吗?实际上,只要是可迭代的对象,也就是具有可返回迭代器的特性,都可以运用这种语法,例如字符串:

> let [a, b, c] = 'XYZ';
undefined
> a;
'X'
> b;
'Y'
> c;
'Z'
>

变量的个数可以少于可迭代的元素数量,多余的元素只是不被理会而已,又或者可以使用 Rest 运算:

> let lt = [10, 9, 8, 7, 6];
undefined
> let [head, ...tail] = lt;
undefined
> head;
10
> tail;
[ 9, 8, 7, 6 ]
>

Rest 运算...会将剩余的元素迭代出来指定给tail,这样的话,写函数式的代码就方便多了,来玩一下:

function sum(numbers) {
    let [head, ...tail] = numbers;
    if(head) {
        return head + sum(tail);
    } else {
        return 0;
    }
}

console.log(sum([1, 2, 3, 4, 5])); // 15

如果可迭代的元素个数少于变量的数量,那么多的变量会是undefined,这是上面的函数得以运作的原因,事实上,参数也可以运用解构语法,只要这么写就好了(要再函数式风格的话):

function sum([head, ...tail]) {
    return head ? head + sum(tail) : 0;
}

console.log(sum([1, 2, 3, 4, 5])); // 15

如果可迭代的元素个数少于变量的数量,也可以指定变量的默认值,例如底下的c会是 3:

let [a, b, c = 3] = [1, 2];

如果只对某几个元素有兴趣呢?空下来就好了:

> let [x, ,y, , z] = [1, 2, 3, 4, 5];
undefined
> x;
1
> y;
3
> z;
5
>

我不赞成这么写就是了,如前面谈到的,它的概念像是模式比对(Pattern match),当你将某个结构拆解并分别指定给变量时,经常出现某种模式时,就可以使用这类语法,因此,像上面的语法,就要检讨一下,你的程序中经常有这种模式吗?不然只会在阅读上造成困惑吧!

当然,也许像函数式之类风格时,就可以运用一下,例如就只是对尾元素感兴趣:

> let [, ...tail] = [1, 2, 3, 4, 5];
undefined
> tail;
[ 2, 3, 4, 5 ]
>

由于有了解构语法,现在可以来玩玩 Python 风格的变量置换:

> let x = 10, y = 20;
undefined
> [x, y] = [y, x];
[ 20, 10 ]
> x;
20
> y;
10
>

在 ES6 中,若是指定的场合,...可以用来当作 Rest 运算,若是放在某个可迭代对象之前,那它可以用来散布(Spread)变量,例如:

> let arr = [1, 2, 3];
undefined
> let arr2 = [...arr, 4, 5];
undefined
> arr2;
[ 1, 2, 3, 4, 5 ]
> function plus(a, b) {
...     return a + b;
... }
undefined
> plus(...[1, 2]);
3
>

在 ES5 时,如果你的实参已经收集为数组了,在〈this 是什么?〉中谈过,可以使用Functionapply方法,而在 ES6 中,如上看到的,直接使用...就可以了。

类似地,对象也可以解构,在过去,如果你经常有以下的模式:

var o = {x : 10, y : 10};
var a = o.x;
var b = o.y;

在 ES6 中,可以写成:

let o = {x : 10, y : 10};
let {x : a, y : b} = o;

唔!x特性会指定给a变量,y特性会指定给b变量,这跟=指定是相反的,一开始有点违反直觉,大概只是这么记:「对象字面量中:左边一直都是特性」。

如果对象上没有对应的特性呢?可以指定默认值:

let o = {x : 10, y : 10};
let {x : a, y : b, z : c = 20} = o;

上面特地让变量与对象特性不同名称,这是为了让你知道谁指定给谁,因为如果变量与对象特性名称相同的话,一开始你可能会搞不清楚谁指定给谁:

let o = {x : 10, y : 10};
let {x : x, y : y} = o;

像这种时候,可以简单写成:

let o = {x : 10, y : 10};
let {x, y} = o;

如果有默认值的话,可以如下:

let o = {x : 10, y : 10};
let {x, y, z = 10} = o;

没有指定默认值的话,z会是undefined,要记得的是,第二行的xy是变量,不是ox特性,因此,o.x被指定为 30 的话,x变量是不受影响的。

对象解构语法也可以用在函数的参数上:

> function foo({x, y}) {
...     console.log(x);
...     console.log(y);
... }
undefined
>
> foo({x : 10, y : 10});
10
10
undefined
>

无论是方才的迭代器解构,或者是对象解构,都可以形成嵌套结构,例如:

let [[x, y, z], b, c] = [[1, 2, 3], 4, 5];

或者是:

let {a: {x, y, z}, b, c} = {a: {x: 10, y: 20, z: 30}, b: 40, c: 50};

再加上默认值、Rest 等语法,可以把它写得很复杂,这你在其他 ES6 的文件中应该有看过,只是我看得头都痛了,要不要写成那样呢?先问问自己在解构变量时,是否真的一而再、再而三的出现某个模式吧!


展开阅读全文