什么是尾调用?

x33g5p2x  于2022-03-11 转载在 其他  
字(1.5k)|赞(0)|评价(0)|浏览(326)

一、写在前面
今天了解一个知识点——尾调用,接下来将详细总结一下我对于尾调用的理解。以及为什么要使用尾调用。
二、什么是尾调用
尾调用就是在函数执行的最后一步执行另一个函数。
如下所示为尾调用:

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

下面的两种情况都不属于尾调用

function f(x){
	let y = g(x)
	return y
}
function f(x){
	return g(x) + 1
}

通过一个例子来看尾调用

function a() {
  return b() + 1
}

function b() {
  return c() + 2
}

function c() {
  return 3
}

console.log(a())  //6
function a() {
  return b(1)
}

function b(num) {
  return c(num + 2)
}

function c(num) {
  return num + 3
}

console.log(a())  //6

上面那种情况不是尾调用,下面的是尾调用。
为什么说尾调用的性能要比没有使用尾调用的性能好呢?
我们都知道在A函数调用时会将A函数放入调用栈中,如果在A函数中存在另一个函数B调用,此时也会将B压入调用栈中,等待函数B调用完成后,此时才可以从调用栈中弹出函数A和函数B。那如果使用尾调用时,当执行函数A时,将函数A压入调用栈中,当执行A时发现存在函数B,并且函数B为函数A的最后一步调用,此时可以将函数A从调用栈中弹出,将函数B压入调用栈中。这样下来性能就做到了优化。
三、尾递归
函数调用自身,叫做递归。如果尾调用自身,则就称为尾递归。
递归非常消耗内存,因为需要同时保存成千上万个调用记录,很容易发生栈溢出的错误。但是对于尾递归来说,由于只存在一个调用记录,所以永远不会发生栈溢出。

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

console.log(factorial(5)) //120

如上述代码所示是一个递归调用求阶乘的算法。但是由于递归可能会导致栈溢出。
尾递归优化

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

console.log(factorial(5, 1))
// factorial(5, 1)
// factorial(4, 5)
// factorial(3, 20)
// factorial(2, 60)
// factorial(1, 120)
// return total

如上述代码所示,我们使用尾递归,fatorial(5, 1)的值也就是factorial(4, 5)的值,同时也是factorial(3, 20)…这样调用栈中,每一次都只有一个函数,不会导致stack overflow
四、对阶乘尾递归的优化
可以对其中一个参数使用默认值,当我们没有输入该参数时,则就会使用默认值。

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

console.log(factorial(5))

五、严格模式
ES6中的尾调用优化只在严格模式下开启,正常模式下无效。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

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

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

相关文章