细说JavaScript中的尾递归

before

先看一个JavaScript中常见的错误

stack_error.png

栈溢出产生的原因

当函数递归调用自身或其他函数过于频繁,而递归调用的层次没有得到正确的终止条件,就可能导致栈溢出。

每次调用一个函数,JavaScript 引擎都会创建一个新的执行上下文,并将其推入调用栈(call stack)。调用栈是一个存储函数调用的栈结构,用于跟踪代码的执行顺序。当一个函数调用另一个函数时,新的执行上下文被推入栈中,当函数执行完成后,执行上下文从栈中弹出。

如果递归调用的层次过深,并且没有正确的终止条件,调用栈会不断增长,最终导致栈溢出。在这种情况下,调用栈的空间被用尽,无法再容纳新的执行上下文。

尾递归是个啥

尾递归是一种特殊形式的递归,指的是递归函数在执行的过程中,递归调用是整个函数体中最后执行的语句。在JavaScript中,尾递归对于优化函数的性能和避免栈溢出很有用。

在尾递归优化中,当前函数调用的返回值直接被传递给下一次递归调用,而不涉及额外的操作。这使得 JavaScript 引擎可以优化,将递归调用的栈帧重用,而不是不断地增加新的栈帧。

尾递归=尾调用+递归

尾调用(tail call)指的是一个函数的最后一条语句是一个返回调用函数的语句.

// 非尾递归,最后一条指令是加法运算 n + xxx,所以不是尾调用
function calc(n, count = 0) {
  if (n === 0) return count;
  return n + calc(n - 1, count)
}
// 尾递归,最后一条指令返回了函数调用,所以这是尾调用
function calc(count, total = 0) {
  if (count === 1) return total;
  return calc(count - 1, total + count)
}

非尾递归的calc函数:

  • 这个函数接受一个参数n,表示要累加的起始数字,以及一个可选参数count,表示当前的累加结果,默认为0。
  • 函数的目的是计算从n开始累加到0的总和。
  • 根据n是否为0来决定递归是否结束,如果n等于0,表示递归结束,直接返回当前的累加结果count
  • 否则,函数进入递归调用,计算n与下一个递归调用结果的总和。这里n每次减1,count则加上当前的n
  • 这个函数的递归调用不是尾调用,因为在递归调用之后还有加法运算,不符合尾调用的定义。

尾递归的calc函数:

  • 这个函数接受两个参数:count表示递减的计数器,total表示累加的总数。
  • count减到1时,递归结束,函数返回当前的总数total
  • 在递归调用时,函数的参数以尾调用的形式传递。计数器count减1,总数total则累加上当前的count
  • 这种递归方式是尾递归,因为最后一条执行的指令是递归调用,符合尾调用的定义。

两者的区别:

  • 非尾递归的calc函数在每次递归调用后执行加法操作,而尾递归的calc函数在递归调用前完成加法操作。
  • 尾递归在某些情况下可以被优化为迭代形式,从而避免栈的溢出问题,提高程序的效率和可维护性。

ES6中的规定

在 ECMAScript 2015(ES6)标准中,引入了对尾调用优化(Tail Call Optimization,TCO)的规范(需要在严格模式下),传送门:262.ecma-international.org/6.0/#sec-ta…

image.png

尾递归与JavaScript执行引擎

所以,尾递归是否能够成功优化取决于具体的 JavaScript 引擎,不同的引擎在对尾递归的处理上可能有差异。

测试方法, 复制以下代码在不同JavaScript环境下执行

'use strict'
function testTCO(enabled) {
  function factorial(n, acc = 1) {
    if (n <= 1) return acc;
    if (enabled) {
      // 尝试使用尾调用优化
      return factorial(n - 1, n * acc);
    } else {
      // 正常的递归调用
      return n * factorial(n - 1, acc);
    }
  }

  try {
    // 尝试一个大的数字,通常足以导致堆栈溢出
    console.log(factorial(100000));
    return true;
  } catch (e) {
    console.log('Stack overflow or other error:', e);
    return false;
  }
}

console.log('Testing with potential TCO:', testTCO(true));
console.log('Testing without TCO:', testTCO(false));

以下是不同的环境下测试的结果

宿主尾递归优化
chrome v120
edge v121
nodejs v18.16
firefox v109
safari v15.4

结论:目前主流浏览器只有Safari浏览器支持尾递归优化, 其他一概不支持, 所以目前来说尾递归实际意义不大,但是没准以后会有用

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值