函数的扩展

92 阅读6分钟

函数参数的默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面,这种写法的好处

  • 相对简洁
  • 方便阅读代码的人可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档
  • 有利于将来的代码优化,即使未来版本彻底拿掉这个参数,也不会导致以前的代码无法运行

参数变量是默认声明的,所以不能在函数体内用 let 或 const 再次声明

参数默认值不是传值的,而是每次都重新计算默认值表达式的值,也就是说,参数默认值是惰性求值的

通常情况下,定义了默认值的参数应该是函数的尾参数,这样比较容易看出参数到底省略了哪些参数。如果非尾部的参数设置默认值,实际使用时,这个参数是无法省略的

指定默认值之后,函数的「length」属性将返回没有指定默认值的参数个数,也就是说,指定了默认值之后,length 属性将失真。如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数

rest 参数

ES6 引入了 rest 参数(形式为 ...变量名),用于获取函数的多余参数,这样就不再需要使用 arguments 对象了

rest 参数搭配的变量是一个数组,该变量将多余的参数放入其中。rest 参数之后不能再有其它参数,否则会报错。

函数的「length」属性不包括 rest 参数

严格模式

ES5 的函数内部是可以设定为严格模式的

ES6 做了修改,规定只要函数参数使用了默认值、解构赋值或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

原因:函数内部的严格模式同时适用于函数体和函数参数。但是函数执行时,先执行函数参数,再执行函数体。这样就有一个不合理的地方:只有从函数体之中才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行

name 属性

函数的 name 属性返回该函数的函数名

将匿名函数赋值给一个变量,ES5 的 name 属性会返回空字符串,ES6 的 name 属性会返回实际的函数名

将具名函数复制给一个变量,ES5 和 ES6 的 name 属性都返回这个具名函数原本的名字

Function 构造函数返回的函数实例,name 属性的值为 anonymous

bind 返回的函数,name 属性返回的值会加上 bound 前缀

function foo() {}

foo.bind({}).name // 'bound foo'
(function() {}).bind({}).name // 'bound '

箭头函数

ES6 允许使用箭头(=>)定义函数

箭头函数的注意事项

  • 函数体内的「this」对象就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当做构造函数,也就是说,不能使用 new 命令,否则会抛出错误
  • 不可以使用 arguments 对象,该对象在函数体内不存在,如果要用,可以用 rest 参数代替
  • 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数

「this」对象的指向是可变的,但是在箭头函数中它是固定的。箭头函数根本没有自己的「this」,导致内部的「this」就是外层代码块的「this」

也正是因为它没有「this」,所以不能用作构造函数

除了「this」,「arguments」、「super」和「new.target」在箭头函数中也是不存在,它们分别指向外层函数的对应变量

箭头函数没有自己的「this」,当然也就不能使用call()apply()bind() 这些方法去改变「this」的指向

绑定 this

函数绑定运算符是并排的双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象作为上下文环境绑定到右边的函数上

foo::bar

// 等同于
bar.bind(foo)

双冒号运算符返回的还是原对象,因此可以采用链式写法

尾调用优化

尾调用:指某个函数的最后一步是调用另一个函数

function f(x) {
    return g(x)
}

函数 f 最后一步调用函数 g,这就是尾调用

尾调用不一定出现在函数尾部,只要是最后一步操作即可

函数调用会在内存中形成一个调用记录,又称调用帧,保存调用位置和内部变量等信息

如果函数 A 的内部调用函数 B,那么会在 A 的调用帧上方形成一个 B 的调用帧,等 B 运行结束,将结果返回给 A,B 的调用帧才会消失。如果函数 B 内部还调用函数 C,那就还有一个 C 的调用帧,以此类推,所有的调用帧就形成一个调用栈

由于尾调用是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不回再用到了,直接用内层函数的调用帧取代外层函数即可

尾调用优化:即只保留内层函数的调用帧

如果所有函数都是尾调用,那么完全可以做到每次执行时调用帧只有一项,这将大大节省内存,这就是尾调用优化的意义

尾递归

函数调用自身称为递归,如果尾调用自身就称为尾递归

递归非常耗内存,因为需要同时保存成百上千个调用帧,很容易发生栈溢出错误。但尾递归只存在一个调用帧,所以永远不会发生栈溢出错误

ES6 的尾调用优化只在严格模式下开启,正常模式下无效

  • 因为,正常模式下函数内部有两个变量,可以跟踪函数的调用栈
    • func.arguments,返回调用时的函数参数
    • func.caller,返回调用当前函数的那个函数

尾调用优化发生事,函数的调用栈会被改写,因此,上面两个变量都会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效

函数参数的尾逗号

函数定义和调用时,不允许最后一个参数有尾逗号,会报错

ES2017 有一个提案,允许函数的最后一个参数有尾逗号