/ ECMAScript6

ECMAScript 6 之函数

标签: ECMAScript 6 javaScript

默认值

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

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

上面代码使用的是结构赋值时的默认值,并没有使用函数传参的默认值。

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

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

函数参数的默认位置

通常情况下,有默认值的参数应该位于函数的参数列表的尾部。因为如果非尾部的参数有默认值的话,该值后面的参数又是必须传递参数的,导致的结果就是这个参数设不设置默认值,都是需要传递参数的。

// 例一
function f(x = 1, y) {
    return [x, y];
}

f()             // [1, undefined]
f(2)            // [2, undefined])
f(, 1)          // 报错
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
    return [x, y, z];
}

f()                // [undefined, 5, undefined]
f(1)               // [1, 5, undefined]
f(1, ,2)           // 报错
f(1, undefined, 2) // [1, 5, 2]

函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

如果设置了默认值的参数不是尾参数,那么之后的参数也不会记录到 length

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

函数参数的作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。

var x = 1

function f(x, y = x) {
  console.log(y)
}

f(2) // 2

感觉上这个例子体现不出这个单独作用域的出现和消失,直接以函数的作用域来理解也可以解释的通。但是下面这个例子就能展现出来了:

let x = 1

function f(y = x) {
  let x = 2
  console.log(y)
}

f() // 1

上面的代码中,函数调用时,参数 y = x 形成一个单独的作用域(这个处在参数初始化阶段,而函数的作用域还没出现)。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x

想象一下,如果这个单独的作用域没出现的话,而是函数的作用域,那么其实上面的代码大概可以等价于:

let x = 1

function f() {
  let y = x
  let x = 2
  console.log(y)
}

f() // Uncaught ReferenceError: x is not defined

那么这个函数是执行不下去的。 let 变量不存在变量提升,所以是会报错的。

当然如果参数的默认值是一个函数,该函数的作用域同样遵守这个规则。

let foo = 'outer'

function bar(func = () => foo) {
  let foo = 'inner'
  console.log(func())
}

bar() // outer

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入门》这本书。