ECMAScript6-函数的扩展

178 阅读5分钟

函数参数的默认值

默认参数作用域与暂时性死区

给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。

function makeKing(name = 'Henry', numerals = 'VIII') {
  return `King ${name} ${numerals}`
}
console.log(makeKing()) // King Henry VIII

因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。

function makeKing(name = 'Henry', numerals = name) {
  return `King ${name} ${numerals}`
}
console.log(makeKing()) // King Henry Henry

参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误:

function makeKing(name = numerals, numerals = 'VIII') {
  return `King ${name} ${numerals}`
}
console.log(makeKing()) // Cannot access 'numerals' before initialization

参数也存在于自己的作用域中,它们不能引用函数体的作用域:

function makeKing(name = 'Henry', numerals = defaultNumeral) {
  let defaultNumeral = 'VIII'
  return `King ${name} ${numerals}`
}

函数参数的默认值与解构赋值的默认值结合使用

function fetch(url, { body = '', method = 'GET', headers = {}}) {
  console.log(method)
}

fetch('http://example.com', {}) // GET
fetch('http://example.com') // 不能省略第二个参数,报错

上面代码,如果函数 fetch 的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时就出现了双重默认值。

双重默认值

function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method)
}
fetch('http://example.com') // GET

上面代码中,函数 fetch 没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量 method 才会取到默认值 GET

触发默认值

如果传入 undefined 将触发该参数等于默认值,null 则没有这个效果。

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

foo(undefined,undefined)  // 5 6
foo(undefined,null) // 5 null

参数默认值应用

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出错误。

function throwIfMissing() {
  throw new Error('Missing parameter')
}
function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided
}

foo()

从上面代码还可以看到,参数 mustBeProvided 的默认值等于 throwIfMissing 函数的运行结果,这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
另外,可以将默认值设为 undefined 表明这个参数是可以省略的。

function foo(optional = undefined) {...}

作用域

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

var x = 1
function f(x, y = x) {
  console.log(y)
}
f(2) // 2

上面代码中,参数 y 的默认值等于变量 x 。调用函数 f 时,参数形成一个单独的作用域。在这个作用域内,默认值变量 x 指向第一个参数 x ,而不是全局变量 x 所以输出 2

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

上面代码中,函数 f 调用时,参数 y = x 形成一个单独的作用域。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x
函数调用时,函数体内部的局部变量 x 影响不到默认值变量 x 。作用域链原理,只能访问外层作用域,不能访问内存作用域。
如果此时全局变量 x 不存在,就会报错。

function f( y = x) {
  let x = 2
  console.log(y)
}
f() // ReferenceError: x is not defined

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

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

上面代码中,函数 bar 的参数 func 的默认值是一个匿名函数,返回值为变量 foo
函数参数形成的单独作用域里面,并没有定义变量 foo,所以 foo 指向外层的全局变量 foo,因此输出 outer

扩展参数

在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素。假设有如下函数定义,它会将所有传入的参数累加起来:

let values = [1, 2, 3, 4]
function getSum() {
  let sum = 0
  for (let i = 0; i < arguments.length; ++i) {
    sum += arguments[i]
  }
  return sum
}

这个函数希望将所有加数逐个传递进来,然后通过迭代 arguments 对象来实现累加。如果不使用扩展操作符,想把定义在这个函数外面的数组拆分,那么就得求助于 apply() 方法:

getSum.apply(null, values) // 10

但在 ES6 中可以通过扩展操作符极为简洁的实现这种操作。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。比如,使用扩展操作符可以将前面例子的数组像这样直接传给函数:

getSum(...values) // 10

剩余参数

收集参数的结果会得到一个实例 Array,是一个数组。

// 顺序累加 values 中的所有值
function getSum(...values) {
  return values.reduce((x,y) => x + y, 0)
}
getSum(1,2,3) // 6

剩余参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为剩余参数的结果可变,所以只能把它作为最后一个参数。

function ignoreFirst(firstValue, ...values) {
  console.log(values)
}

ignoreFirst() // []
ignoreFirst(1) // []
ignoreFirst(1, 2) // [2]
ignoreFirst(1, 2, 3) // [2,3]

箭头函数

箭头函数的this

箭头函数不绑定 this ,其 this 就是外层作用域(首先要有作用域)的 this。 箭头函数中的 this 指向外层作用域的 this ,而在这里它的外层 {} 不构成单独的作用域,所以继续找,直到找到全局作用域 window

let obj = {
  log:function() {
    setTimeout(()=> {
      console.log(this) // obj log是个函数,函数有作用域,所以 this 在 log 这找到了
    },1000)
    
    setTimeout(function() {
      console.log(this) // window
    },1000)
  }
}

obj.log()

arguments

箭头函数没有 arguments。 因为箭头函数没有 arguments,自身找不到 arguments 会向上层作用域查找,此处会去 window 中查找,window 中没有 arguments 属性,所以报错。node 中有 arguments

var foo = ()=> { console.log(arguments) }
foo() // arguments is not defined

简化回调函数

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

let getTempItem = id => ({ id: id, name: "Temp" })

重新认识ES6中的语法糖 原文链接