Skip to content

函数扩展

参数默认值

ES6 - 定义函数可以指定默认值。

  • 参数变量是默认声明,不可用letconst再次声明
  • 函数不能有同名参数
  • 参数为惰性求值,每次都要重新计算
  • 传入undefined,将触发该参数等于默认值,null则没有这个效果
js
let x = 99
function foo(p = x + 1) {
  console.log(p)
}
foo() // 100
x = 100
foo() // 101

参数默认值与解构

下面写法:不能省略第二个参数,对象中的属性有默认值,但是对象本身没有

js
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method)
}
// 正确写法
// { body = '', method = 'GET', headers = {} } = {}
js
// 解构的值赋予默认值
{x = 0, y = 0} = {} // {} - 0,0

// 解构的对象赋予默认值
{x, y} = { x: 0, y: 0 } // {} - undefined,undefined

参数默认值位置

定义了默认值的参数,应该是尾参数。非尾参数,用undefined占位

js
function f( x = 1, y ){}
f() // 1 undefined
f(,1) // 报错
f(undefined,1) // 1 1
f(2,1) // 2 1

函数 length 属性

函数length属性:将返回没有指定默认值的参数个数

rest参数也不会计入 length。

默认值参数不是尾参数,length 不会计入后面的参数

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

    function (...args) {}
  )
  .length(
    // 0

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

作用域

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

js
// y = x 形成 单独作用域
// 实际执行 let y = x

function f(y = x) {
  let x = 0
  console.log(x, y)
}
f(1)
js
function bar(func = () => foo) {
  let foo = 'inner'
  console.log(func())
}

bar() // ReferenceError: foo is not defined

y默认值为匿名函数,其中x指向同作用域第一个参数x

foo函数内部变量 x 是与 foo 参数变量 x 不同

执行 y() 只会影响参数变量外部变量内部变量都不会改变

js
var x = 1
function foo(
  x,
  y = function () {
    x = 2 // closure
  }
) {
  // local x - undefined
  var x = 3
  // local x - 3
  y()
  // local x - 3
  console.log(x)
}
foo()
// global x - 1

与上面不同的是,没有内部变量,此时x=3中的x为参数变量。

js
var x = 1
function foo(
  x,
  y = function () {
    x = 2
  }
) {
  // x - undefined
  x = 3
  // x - 3
  y()
  // x - 2
  console.log(x)
}

foo()
x // x - 1

应用

参数默认值在运行时执行,利用参数默认值,让不填参数就报错

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

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

foo()
// Error: Missing parameter

rest 参数

ES6 中,引入 rest 参数 (...变量名),用于获取多余参数,代替arguments

arguments:类数组对象

rest:数组,无需转换

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

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

rest 参数后不能有其他参数,即rest 参数为最后一个参数

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

函数length属性,不包括 rest 参数

js
;(function (a) {}
  .length(
    // 1
    function (...a) {}
  )
  .length(
    // 0
    function (a, ...b) {}
  ).length) // 1

严格模式

ES5,内部可以设定严格模式

js
function doSomething(a, b) {
  'use strict'
  // code
}

ES2016,函数参数使用默认值,解构赋值,扩展运算符后,不能显示定义为 严格模式

严格模式适用于 函数内部和函数参数

原因:只有在函数体内才能知道是否以严格模式执行,但参数执行先于函数内部

js
// 报错
function doSomething(value = 070) {
  'use strict';
  return value;
}

报错分析:严格模式下不能使用前缀 0表示八进制。执行value=070,进入函数内部,发现严格模式执行,所以报错。

解决方案

全局性严格模式

js
'use strict'

function doSomething(value = 070) {
  return value
}

包裹在无参数的立即执行函数里面

js
const doSomething = (function () {
  'use strict'
  return function (value = 42) {
    return value
  }
})()

name 属性

匿名函数

js
var f = function () {}
// ES5
f.name // ""
// ES6
f.name // "f"

具名函数

赋值给变量,name属性为函数名(ES5,ES6 相同)

js
const bar = function baz() {}
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"

对象方法

使用了取值函数(getter)和存值函数(setter),函数名前加 getset

js
const obj = {
  get foo() {},
  set foo(x) {}
}

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo')

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

对象方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述

js
const key1 = Symbol('description')
const key2 = Symbol()
let obj = {
  [key1]() {},
  [key2]() {}
}
obj[key1].name // "[description]"
obj[key2].name // ""

构造函数:返回函数实例,name属性为anonymous

js
new Function().name // "anonymous"

bind:返回函数,name属性加上bound前缀

js
function foo() {}
foo
  .bind({})
  .name(
    // "bound foo"
    function () {}
  )
  .bind({}).name // "bound "

箭头函数

ES6:箭头(=>)定义函数

基础用法

函数参数:不需要参数多个参数,圆括号代表参数部分

js
;() => 5
;(x, y) => x + y

代码块:多于一条语句,对象外加{},并且 return 语句返回

js
;(num1, num2) => {
  return num1 + num2
}

返回对象:因为大括号解析为代码块,箭头函数直接返回一个对象时,外层加上()

js
() => { id: id, name: "Temp" } // 报错
() => ({ id: id, name: "Temp" }) // 正确

特殊情况:{}被解析为代码块,函数内部执行 a:1,无返回值

js
let foo = () => {
  a: 1
}
foo() // undefined

无需返回值:单行语句,且不需要返回值

js
let fn = () => void doesNotReturn()

解构、rest 参数

js
;({ first, last }) => first + ' ' + last
;(...nums) => nums

注意点

  1. this 指向固定,定义时所在对象,而不是使用者所在对象
  2. 箭头函数没有自己的 this,导致内部 this 指向外部 this。没有 this,不能用 call apply bind 方法改变 this 指向
  3. 不可做构造函数,不可以使用new命令
  4. 没有arguments对象(不存在),使用rest参数代替
  5. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数
  6. super、new.target 在箭头函数中也是不存在,指向外层变量
  7. typeof 判断箭头函数 结果为function
  8. instanceof 判断是否Function实例 结果为 true
js
function Timer() {
  this.s1 = 0
  this.s2 = 0
  // 箭头函数 this -> timer
  setInterval(() => this.s1++, 1000)
  // 普通函数 this -> window
  setInterval(function () {
    this.s2++
  }, 1000)
}

var timer = new Timer()

setTimeout(() => console.log('s1: ', timer.s1), 3100)
setTimeout(() => console.log('s2: ', timer.s2), 3100)
// s1: 3
// s2: 0

下面代码有几个this

js
function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id)
      }
    }
  }
}

var f = foo.call({ id: 1 })

var t1 = f.call({ id: 2 })()() // id: 1
var t2 = f().call({ id: 3 })() // id: 1
var t3 = f()().call({ id: 4 }) // id: 1

只有一个this,即foothis,无论怎么嵌套,都会去获取 foo 中的 id

不适用场合

以下两种方法应使用传统写法,而非箭头对象

定义对象的方法

对象中使用箭头函数:函数会指向对象所在作用域,此处是window,普通函数指向obj

js
globalThis.s = 21 // globalThis 此处 -> window
const obj = {
  s: 42,
  m: () => console.log(this.s)
}

obj.m() // 21

动态 this

this 应该动态指向,使用箭头函数后,this 固化。

js
var button = document.getElementById('press')
button.addEventListener('click', () => {
  this.classList.toggle('on')
})

嵌套箭头函数

部署管道机制(pipeline)

js
const pipeline =
  (...funcs) =>
  (val) =>
    funcs.reduce((a, b) => b(a), val)

[?] λ 演算

js
// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

尾调用优化

尾调用

尾调用(Tail Call):函数的最后一步是调用另一个函数

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

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

js
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x)
}

非尾调用

js
// 情况一 - 函数之后还有别的操作
function f(x) {
  let y = g(x)
  return y
}

// 情况二 - 函数之后还有别的操作
function f(x) {
  return g(x) + 1
}

// 情况三
function f(x) {
  g(x)
  // return undefined
}

尾调用优化

调用帧:函数调用会在内存形成调用记录,保存调用位置和内部变量信息。

调用栈:函数 A 调用 B,会在 A 上方形成 B 调用帧。B 返回结果后,B 调用帧消失。如果 B 调用 C,会 B 上方形成 C 调用帧。所有调用帧组成调用栈。

尾调用是函数最后一步操作,无需保留调用帧。因为调用位置、内部变量等信息都不会再用到。

尾调用优化(Tail call optimization):保留内层函数的调用帧。如果所有函数都是尾调用,每次执行时,调用帧只有一项,这将大大节省内存。

注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

尾递归

问题:递归需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)

解决:函数尾调用自身,就称为尾递归。

尾递归关键:每次递归都在收集结果,避免线性递归展开消耗内存,只存在一个调用帧。

尾递归:只有一个调用帧

js
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  }
  return Fibonacci2(n - 1, ac2, ac1 + ac2)
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

非尾递归:2 的 n 次方个调用帧

js
function Fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return Fibonacci(n - 1) + Fibonacci(n - 2)
}

Fibonacci(10) // 89
Fibonacci(100) // 超时
Fibonacci(500) // 超时

递归函数改写

所有用到的 内部变量 改写成 函数的参数。

缺点:不太直观,计算 5 阶乘,为何传 1

js
function tailFactorial(n, total) {
  if (n === 1) return total
  return tailFactorial(n - 1, n * total)
}

tailFactorial(5, 1) // 120

方案 1:外部提供正常函数

js
function factorial(n) {
  return tailFactorial(n, 1)
}

factorial(5) // 120

函数柯里化:多参数函数转为单参数函数

js
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n)
  }
}

const factorial = currying(tailFactorial, 1)
factorial(5) // 120

方案 2:ES6 默认值

js
function factorial(n, total = 1) {
  if (n === 1) return total
  return factorial(n - 1, n * total)
}

factorial(5) // 120

严格模式

尾调用优化之只在严格模式下有用

因为正常模式下,存在func.arugmentsfunc.caller,尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾递归优化实现

问题:尾递归优化只在严格模式下生效

解决:正常模式下,或者那些不支持该功能的环境中。通过采用 “循环” 换掉 “递归” ,减少调用栈

js
function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  } else {
    return x
  }
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

蹦床函数(trampoline)可以将递归执行转为循环执行。这里是返回一个函数,然后执行函数,并非函数中调用函数,避免了递归执行。

js
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f()
  }
  return f
}

通过蹦床函数优化问题代码

js
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1)
  } else {
    return x
  }
}

通过 sum 使每次返回一个新的函数,不会发生栈溢出

js
trampoline(sum(1, 100000)) // 100001

==@DIF 尾调用优化:active 原理==

js
function tco(f) {
  var value
  var active = false
  var accumulated = []

  return function accumulator() {
    accumulated.push(arguments)
    if (!active) {
      active = true
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift())
      }
      active = false
      return value
    }
  }
}

var sum = tco(function (x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  } else {
    return x
  }
})

sum(1, 100000)

函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)

Function.prototype.toString()

Function.prototype.toString():返回函数代码本身,以前会省略注释和空格,ES2019 - 后不会省略注释。

js
function /* foo comment */ foo() {}
foo.toString()
// "function /* foo comment */ foo () {}"

catch 命令的参数省略

catch代码块参数不可省略。

js
try {
  // ...
} catch (err) {
  // 处理错误
}

ES2019 - 允许catch语句省略参数。

js
try {
  // ...
} catch {
  // ...
}

相关链接

[-] 函数扩展