什么是尾调用?

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

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

  1. function f(x) {
  2. return g(x)
  3. }

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

  1. function f(x){
  2. let y = g(x)
  3. return y
  4. }
  1. function f(x){
  2. return g(x) + 1
  3. }

通过一个例子来看尾调用

  1. function a() {
  2. return b() + 1
  3. }
  4. function b() {
  5. return c() + 2
  6. }
  7. function c() {
  8. return 3
  9. }
  10. console.log(a()) //6
  1. function a() {
  2. return b(1)
  3. }
  4. function b(num) {
  5. return c(num + 2)
  6. }
  7. function c(num) {
  8. return num + 3
  9. }
  10. console.log(a()) //6

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

  1. function factorial(n) {
  2. if (n === 1) return 1
  3. return n * factorial(n - 1)
  4. }
  5. console.log(factorial(5)) //120

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

  1. function factorial(n, total) {
  2. if (n === 1) return total
  3. return factorial(n - 1, total * n)
  4. }
  5. console.log(factorial(5, 1))
  6. // factorial(5, 1)
  7. // factorial(4, 5)
  8. // factorial(3, 20)
  9. // factorial(2, 60)
  10. // factorial(1, 120)
  11. // return total

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

  1. function factorial(n, total = 1) {
  2. if(n === 1) return total
  3. return factorial(n - 1, total * n)
  4. }
  5. console.log(factorial(5))

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

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

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

相关文章