ES6中的...,默认参数和解构赋值

ES6 引入了一个新的操作符 ... ,根据它的使用情况决定是展开或者折叠操作数。

展开/折叠

让我们看下面这段代码

1
2
3
4
5
function foo(x,y,z) {
console.log( x, y, z );
}

foo( ...[1,2,3] ); // 1 2 3

... 操作符出现在数组前面,它表现为将数组展开成一个个的数。

这种使用方式最典型的情况就是将数组展开成函数的参数。这种使用方式和 apply 类似:

1
foo.apply( null, [1,2,3] );     // 1 2 3

同时 ... 还可以用作将数组展开到别的上下文中,例如展开到别的数组当中:

1
2
3
4
var a = [2,3,4];
var b = [ 1, ...a, 5 ];

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

这种使用方式与 [1].concat(a,[5]) 一样。

另外一种使用方式,与上面的情况相反,它是将多个数值折叠到一个变量当中,变成一个数组。如下所示:

1
2
3
4
5
function foo(x, y, ...z) {
console.log( x, y, z );
}

foo( 1, 2, 3, 4, 5 ); // 1 2 [3,4,5]

...z 的意思是将除了x,y 两个之外的参数聚合到数组 z 当中来。 注意这里与第一个例子的区别,展开是函数调用时,将传入的数组参数展开;折叠是函数声明时,将调用可能传入的其他参数聚合到变量中。

所以上面这个例子是 x 赋值为 1y 赋值为 2, 其他的 3,4,5 聚合到数组 z 中来。

当然如果没有其他任何命名的参数, ... 会把所有参数折叠成一个数组:

1
2
3
4
5
function foo(...args) {
console.log( args );
}

foo( 1, 2, 3, 4, 5); // [1,2,3,4,5]

...args 一般被叫做剩余参数(rest parameters),因为你可以收集剩余的参数,或者说聚集,折叠这些参数。

这种使用方式是JS中以前的类数组参数 arguments 的一个有效的替代。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// doing things the new ES6 way
function foo(...args) {
// `args` is already a real array

// discard first element in `args`
args.shift();

// pass along all of `args` as arguments
// to `console.log(..)`
console.log( ...args );
}

// doing things the old-school pre-ES6 way
function bar() {
// turn `arguments` into a real array
var args = Array.prototype.slice.call( arguments );

// add some elements on the end
args.push( 4, 5 );

// filter out odd numbers
args = args.filter( function(v){
return v % 2 == 0;
} );

// pass along all of `args` as arguments
// to `foo(..)`
foo.apply( null, args );
}

bar( 0, 1, 2, 3 ); // 2 4

默认参数

相信下面这种默认参数的使用方式,你肯定不陌生:

1
2
3
4
5
6
7
8
9
10
11
function foo(x,y) {
x = x || 11;
y = y || 31;

console.log( x + y );
}

foo(); // 42
foo( 5, 6 ); // 11
foo( 5 ); // 36
foo( null, 6 ); // 17

但是使用上面的代码,你会遇到一些问题:

1
foo( 0, 42 );       // 53 <-- Oops, not 42

为了修复这个问题,可以做一个更加细致的检查

1
2
3
4
5
6
7
8
9
function foo(x,y) {
x = (x !== undefined) ? x : 11;
y = (y !== undefined) ? y : 31;

console.log( x + y );
}

foo( 0, 42 ); // 42
foo( undefined, 6 ); // 17

下面我们看看ES6中加入的默认参数值的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(x = 11, y = 31) {
console.log( x + y );
}

foo(); // 42
foo( 5, 6 ); // 11
foo( 0, 42 ); // 42

foo( 5 ); // 36
foo( 5, undefined ); // 36 <-- `undefined` is missing
foo( 5, null ); // 5 <-- null coerces to `0`

foo( undefined, 6 ); // 17 <-- `undefined` is missing
foo( null, 6 ); // 6 <-- null coerces to `0`

注意上面这些函数参数中细微的差别导致的结果的不同。

x=11 比起 x||11 更像是 x !== undefined ? x : 11 ,所以在使用的过程中要注意这一点。

默认表达式值

函数默认参数值不仅仅是一些简单的字面量,而且还可以是一个有效的表达式,甚至是一个函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function bar(val) {
console.log( "bar called!" );
return y + val;
}

function foo(x = y + 3, z = bar( x )) {
console.log( x, z );
}

var y = 5;
foo(); // "bar called"
// 8 13
foo( 10 ); // "bar called"
// 10 15
y = 6;
foo( undefined, 10 ); // 9 10

可以看到这些参数值会在需要的时候进行计算得到,什么叫需要的时候?就是传入的参数省略,或者是 undedined 的时候

要注意到一个细节,函数声明中的参数是在它们自己的作用域而不是函数体的作用域,也就是说,函数参数表达式的变量,首先匹配前面的变量,然后再匹配外部变量。如下的情况:

1
2
3
4
5
6
7
var w = 1, z = 2;

function foo( x = w + 1, y = x + 1, z = z + 1 ) {
console.log( x, y, z );
}

foo(); // ReferenceError

w+1 中的 w 首先在括号内找 w,但是没找到,于是它去括号外面找。而 x+1 中的 x 已经在括号内找到,就不要到括号外面去找。

z+1中的z在括号内找到了,但是呢没有被初始化。于是就会引起一个错误。

解构赋值

ES6引入了一个语法特性叫做解构赋值,看下面的代码

1
2
3
4
5
6
7
8
function foo() {
return [1,2,3];
}

var tmp = foo(),
a = tmp[0], b = tmp[1], c = tmp[2];

console.log( a, b, c ); // 1 2 3

我们把 foo() 函数返回的结果数组分别赋值给变量 a, b, c 。这种赋值叫做结构化赋值(structured assignment

还有一种对象结构化赋值:

1
2
3
4
5
6
7
8
9
10
11
12
function bar() {
return {
x: 4,
y: 5,
z: 6
};
}

var tmp = bar(),
x = tmp.x, y = tmp.y, z = tmp.z;

console.log( x, y, z ); // 4 5 6

我们看看解构赋值的形式:

1
2
3
4
5
var [ a, b, c ] = foo();
var { x: x, y: y, z: z } = bar();

console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6

对象属性赋值模式

让我们深入挖掘一下之前的 {x:x,...} 语法,如果属性名和你想要赋值的属性名一致,则可以写得更加简洁:

1
2
3
var { x, y, z } = bar();

console.log( x, y, z ); // 4 5 6

有时候我们希望赋值给不同的变量,那么可以使用一种冗长的形式:

1
2
3
4
var { x: bam, y: baz, z: bap } = bar();

console.log( bam, baz, bap ); // 4 5 6
console.log( x, y, z ); // ReferenceError

要注意上面的一个细节,冒号左边是值,右边的要赋值的变量。

不仅仅是声明

解构是一个一般性的赋值操作,不仅仅是一个声明:

1
2
3
4
5
6
7
var a, b, c, x, y, z;

[a,b,c] = foo();
( { x, y, z } = bar() );

console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 6

在上面的例子中,变量已经被声明了,我们做的是赋值。

配合...

解构赋值还可以和 ... 操作符结合:

1
2
3
4
var a = [2,3,4];
var [ b, ...c ] = a;

console.log( b, c ); // 2 [3,4]

这里 ... 可以将数组解构,在ES6中并不能用于对象的解构

配合默认值

解构也可以提供默认值:

1
2
3
4
5
6
7
8
9
var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();

console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, w ); // 4 5 6 20

var { x, y, z, w: WW = 20 } = bar();

console.log( x, y, z, WW ); // 4 5 6 20

对象字面量表达式

ES6对对象字面量增加了一些扩展。 如果属性名和变量名一样,可以像上面一样使用简洁的赋值方式:

1
2
3
4
5
var x = 2, y = 3,
o = {
x,
y
};

如果属性值是函数,可以简单的这么定义:

1
2
3
4
5
6
7
8
var o = {
x() {
// ..
},
y() {
// ..
}
}

总结

本文介绍了ES6中的 ... 与解构赋值。 ... 在不同情况下有不同的表现,有时是折叠操作数,有时是展开操作数。在函数声明的时候是聚集传过来的操作数,在函数调用的时候是将数组展开为参数传入函数。