ECMAScript 6 之函数

设置默认值

想必在写函数时,都会有一个困惑:要是在这个函数调用的时候,参数没有传入时,该怎么办?设置默认值。对,所以大部分的函数都会有一下的代码。

function f(a,b){  
    a = a || {/*...*/}
    /* ... */
}

以上代码相信绝大多数的 javascript 开发者都写过,乍一看是没啥大问题的,但是如果传入的值是 false 呢?那么 a 就会被替换成默认值。为了避免这个问题就应该将代码改一改:

if( typeof a === 'undefined' ){  
    a = {/* ... */}
}

这就大大增加了代码量,而且一个痛点莫名其妙的增加了:增加了代码的不可读性。

ES6 中允许为函数设置默认值,而且定义明确,代码的可读性大大增加。

function log(x, y = 'World') {  
  console.log(x, y);
}

log('Hello') // Hello World  
log('Hello', 'China') // Hello China  
log('Hello', '') // Hello  

这让函数体内(也就是 {} 内)的代码专注于函数实现,而不用去处理默认值的逻辑。

当然在一个构造函数中也可以使用。

function Point(x = 0, y = 0) {  
  this.x = x;
  this.y = y;
}

var p = new Point();  
p // { x: 0, y: 0 }  

解构赋值与默认值结合使用

如果不理解 解构赋值 的含义,可以先去之前的文章中查看。 不多说,先上个例子:

function foo({x, y = 5}) {  
  console.log(x, y);
}

foo({}) // undefined, 5  
foo({x: 1}) // 1, 5  
foo({x: 1, y: 2}) // 1, 2  
foo() // TypeError: Cannot read property 'x' of undefined  

上面代码使用的是结构赋值时的默认值,并没有使用函数传参的默认值,具体的请看,之前写的 ECMAScript 6 之解构变量 一文。

在明白了上面这个例子后,在来看这个例子:

function foo({x, y = 5} = {x: 4, y: 7}) {  
  console.log(x, y);
}

foo({}) // undefined, 5  
foo({x: 1}) // 1, 5  
foo({x: 1, y: 2}) // 1, 2  
foo() // 4, 7  

唯一的不同就是,在函数参数的后面多了 = {x: 1, y: 2} ,造成的结果就是在 foo 函数调用的第 4 种方式上的结果。很明显,第四种方式,并没有传递参数,就使用了我们规定好的默认值。

那么现在我们就可以用两种方式来规定一个值的默认值了,假设一个场景,我们的函数需要一个含有 xy 的对象,当有参数传入时, y 的默认值是 5 ,而没有参数传入时,默认值为 {x: 4, y: 7} ,就可以用上面的函数实现,避免了很多不必要的逻辑判断代码去为函数参数设置默认值。

应用

利用默认值来指定一个参数不可省略:

function throwIfMissing() {  
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {  
  return mustBeProvided;
}

foo()  
// Error: Missing parameter

上面的代码如果没有foo传递实际的参数,就回去调用默认的函数,就会抛出一个错误。对上面的 throwIfMissing 函数进行进一步的改造,就能抛出更具体的错误信息了。

function throwIfMissing(argName = '') {  
  throw new Error('Missing '+ argName +' parameter');
}

function foo(arg1 = throwIfMissing('arg1')) {  
  return arg1;
}

foo()  
// Error: Missing arg1 parameter

REST 参数

ES6 引入了 REST 参数(即 "...参数名" ),用于获取函数的多余参数,在一定情况下减少了对 arguments 对象的使用了。

function add(...values) {  
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10  

由于 arguments 是函数体内的一个变量,所以必须写在函数体内(也就是 {} 内),而 REST 参数写在函数参数里,使用起来便捷。以下就是使用 argumentsREST 参数实现函数参数排序两种不同的方式。

// arguments变量的写法
function sortNumbers() {  
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();  

很明显,结合使用 ES6 的写法,使用 REST 参数方式更加的语义化和便捷。

REST 参数表示剩下的参数,所以在 REST 参数前是可以有别的参数存在的,但是在 REST 参数之后是不可以在有函数参数的。并且 REST 参数代表的是一个数组,故所有的数组的方法都使用于这个变量。

function push(array, ...items) {  
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];  
push(a, 1, 2, 3)

// 报错
function f(a, ...b, c) {  
  // ...
}

扩展运算符

扩展运算符是 REST 参数的逆,将一个数组转化为用逗号分隔的参数序列。

console.log(...[1, 2, 3])  
// 1 2 3

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

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

主要用于函数调用。

function push(array, ...items) {  
  array.push(...items);
}

function add(x, y) {  
  return x + y;
}

var numbers = [4, 38];  
add(...numbers) // 42  

扩展运算符的应用

  • 替代apply方法
// ES5的写法
Math.max.apply(null, [14, 3, 77])

// ES6的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);  

由于 Math.max 需要一系列单独的数字,而不是一个数字,在 ES5 中只能使用 apply 方法来实现,而在 ES6 中使用扩展运算符就方便的多了。

像数组的 push 方法也使用

// ES5的写法
var arr1 = [0, 1, 2];  
var arr2 = [3, 4, 5];  
Array.prototype.push.apply(arr1, arr2);

// ES6的写法
var arr1 = [0, 1, 2];  
var arr2 = [3, 4, 5];  
arr1.push(...arr2);  
  • 合并数组,不多说,直接撸代码
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

var arr1 = ['a', 'b'];  
var arr2 = ['c'];  
var arr3 = ['d', 'e'];

// ES5的合并数组
arr1.concat(arr2, arr3);  
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
  • 与解构赋值结合
// ES5
a = list[0], rest = list.slice(1)  
// ES6
[a, ...rest] = list

alist 数组中的第一个,而 rest 取剩下的所有。 ES5ES6 实现的区别。同样的 REST 之后不可以用别的解构的值,并且只能在数组中使用。

  • 函数的返回值,在 js 中若需要返回多指时,只能使用对象或是数组,而扩展运算符本质上是数组,所以也可以用着函数的返回值中。
function test(...args){  
    return ...args;
}

test([a, b, c]);  
// [a, b, c]
  • 字符串,将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组。
let map = new Map([  
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]  

箭头函数

感觉这点时 EMCAScript 6 中极其重要的一点,所以单独会有一节。

总结:主要是函数参数的一些注意点,以及一些新特性,由于箭头函数在 ES6 中应该会成为随手就写的编程方式,所以单独开一篇。

注: 上面的例子大多数来自 《ECMAScript 6入门》这本书。