前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >javascript尾递归优化_2023-02-27

javascript尾递归优化_2023-02-27

原创
作者头像
用户10377405
发布2023-02-27 14:38:30
4290
发布2023-02-27 14:38:30
举报
文章被收录于专栏:前端面试题18

JS中的递归

我们来看一个阶乘的代码
代码语言:javascript
复制
function foo( n ){
  if(n <= 1){
    return 1;
  }
  return n * foo( n - 1 );
}

foo(5);  // 120
下面分析一下,代码运行过程中,执行上下文栈是怎么变化的
  1. 这个代码是在全局作用域中执行的,所以在foo函数得到执行之前,上下文栈中就已经被放入了一个全局上下文。之后执行一个函数,生成一个新的执行上下文时,JS引擎都会将新的上下文push到该栈中。如果函数执行完成,JS引擎会将对应的上下文上下文栈中弹出
  2. 一开始执行foo函数的时候,JS引擎会创建foo的执行上下文,将该执行上下文push进上下文栈。然后开始执行foo中的代码。
在这里插入图片描述
在这里插入图片描述

现在上下文栈中已经有了两个执行上下文了

  1. 在执行到foo中代码快结束时,return表达式中,又调用了foo函数。所以又会创建一个新的执行上下文。并且JS引擎会把这新的执行上下文push到上下文栈中。
在这里插入图片描述
在这里插入图片描述

现在上下文栈中已经有了三个执行上下文了

  1. 开始重复第3步的执行。一直到n<=1,才不会有新的执行上下文产生。

此刻上下文栈中,已经有了6个上下文了(包含了全局上下文)

在这里插入图片描述
在这里插入图片描述
设想一下
  1. 如果刚开始调用的时候,传入n的初始值为100,到n<=1时,上下文栈中会有几个上下文。101个。
  2. 如果初始值为1000呢?到n<=1时,会有1001个执行上下文
  3. 也就是说,传入的初始值越大,执行上下文栈中,就会有越多的执行上下文🥲
  4. 对于上下栈,它的空间是有限的,一旦存放的上下文占用内存产出了它的最大内存,就会出现栈溢出。RangeError: Maximum call stack size exceeded
  5. 而在chrome中,不仅会对栈的空间有限制,还会对函数的递归次数有限制

递归优化

我们来看一个样例代码
代码语言:javascript
复制
function outer() {
    return inner();
}

outer();

分析一下,这里的上下文栈是怎么变化的

  1. 调用outer函数的时候,第二个栈帧被推到了栈上。

第一个栈帧是全局上下文

把上下文栈中的一个上下文称作一个栈帧

在这里插入图片描述
在这里插入图片描述
  1. 执行到了return语句,必须要计算inner调用结果,才能返回值
  2. 调用inner函数,第三个栈帧被推入到栈上。
在这里插入图片描述
在这里插入图片描述
  1. 执行inner函数,将返回值传回到outer函数。inner执行完毕。第三个栈帧被弹出栈
在这里插入图片描述
在这里插入图片描述
  1. outer函数再返回值。outer函数执行完毕,第二个栈帧被弹出栈
在这里插入图片描述
在这里插入图片描述
等等,情况不是一样的么?优化在哪里
  1. 在执行到outer中的return语句的时候,要先计算inner函数的值。这时候JS引擎发现,把第二个栈帧弹出去也没有关系。因为到时候,直接拿inner的返回值返回出去就好了,第二个栈帧就没有必要保存了。
  2. 将第二个栈帧弹出

这个时候,栈中只有一个栈帧了--全局上下文

  1. 执行到inner函数,inner函数的上下文被push到栈中
在这里插入图片描述
在这里插入图片描述

这个时候,栈中有两个栈帧了

  1. 开始执行inner函数,计算返回值后,inner函数执行完毕。inner的上下文栈帧被弹出栈。
在这里插入图片描述
在这里插入图片描述

栈中又只剩一个栈帧了--全局上下文

综上,我们可以看出:如果没有优化,没多调用一次嵌套函数,就会多增加一个栈帧;有了优化之后,无论调用多少次嵌套,栈中只会有两个栈帧。这就是ES6尾调用优化的关键😄

递归优化的条件

  1. 代码在严格模式下执行
  2. 外部函数的返回值,是对尾调用函数的调用
  3. 尾调用函数返回后,不需要执行额外的逻辑
  4. 尾调用函数不是外部函数作用域中自由变量的闭包
下面是《高程》里面的示例,帮助大家理解
代码语言:javascript
复制
// 无优化: 尾调用没有返回
function outer(){
  inner();
}

// 无优化: 尾调用没有直接返回
function outer(){
  let innerResult = inner();

  return innerResult;
}

//无优化: 尾调用返回值后,必须要转型为字符串
function outer(){
  return inner().toString(); 
}

// 无优化: 尾调用是一个闭包
function outer(){
  let foo = 'bar';

  function inner(){ return foo; }

  return inner();
}

其实我觉得上面的倒数第二个,它是完全可以尾调用优化的。因为这个计算是不需要外部函数的上下文里面内容支持的。可能是这样的计算必须要在外部函数的上下文中完成吧,咱也不懂。记一下吧。

有哪位同仁能够帮我解答一下我这个问题吗😁

实操一个优化代码

下面是一个普通的求斐波那契数列的函数
代码语言:javascript
复制
function fib( n ){
  if( n < 2){
    return n;
  }

  return fib(n - 1) + fib(n - 2)
}

这是一个非常简单的斐波那契数列的函数,可以看到它不符合尾递归的条件。因为返回值的调用函数参与了额外计算

我们来优化一下
代码语言:javascript
复制
function fib( n ){
  return fibImpl(0, 1, n);
}

function fibImpl(a, b, n){
  if(n === 0){
    return a;
  }

  return fibImpl(b, a+b, n-1);
}

看,这样是不是就符合尾递归调用函数了

简单讲解一下上面的代码

  1. 把原先的一个函数拆成了两个
  2. 第一个函数接受一个参数。这个参数表示求第几位的斐波那契数。
  3. 第二个参数接收三个参数。前两个参数表示正在计算的两个位置的数字,第三个参数表示还要计算多少次

斐波那契数规律,就是从第三位开始,每一位的数字都是前两位数字的和

那上面的计算的阶乘代码怎么优化呢?
代码语言:javascript
复制
function foo( n ){
  if(n <= 1){
    return 1;
  }
  return n * foo( n - 1 );
}

foo(5);  // 120

这个是上面计算阶乘的代码,我们可以用同样的思路,来对其做尾递归函数优化

代码语言:javascript
复制
function foo( n ){
  return inner(n, n - 1)
}

function inner(sum, n){
  if(n <= 1){
    return sum;  
  }
  return inner(sum * n , n -1);
}

foo(5);

是不是超简单😁

最新版的浏览器已经支持尾递归

可以在计算斐波那契数列的时候,比较尾递归和非尾递归的时间。相信你会和我一样,会不由自主的感叹

总结

  1. JS中的递归函数调用的时候,上下文栈是怎么变化的
  2. 什么是递归优化
  3. 递归优化的条件是什么
  4. 手动优化一个递归代码

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JS中的递归
    • 我们来看一个阶乘的代码
      • 下面分析一下,代码运行过程中,执行上下文栈是怎么变化的
        • 设想一下
        • 递归优化
          • 我们来看一个样例代码
            • 等等,情况不是一样的么?优化在哪里
            • 递归优化的条件
              • 下面是《高程》里面的示例,帮助大家理解
              • 实操一个优化代码
                • 下面是一个普通的求斐波那契数列的函数
                  • 我们来优化一下
                    • 那上面的计算的阶乘代码怎么优化呢?
                    • 最新版的浏览器已经支持尾递归
                    • 总结
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档