JS 递归调用溢出

前言

本来我是用js编程一道题,使用了递归,结果浏览器报错RangeError: Maximum call stack size exceeded
意思也就是最大的调用栈规格超出了,我隐隐知道是怎么回事了,估计是存放 call 的 stack 容量不够了。
这涉及到浏览器对 js 的内存分配情况了,每个浏览器对各自对 js 实现方式不一样,js 内存分布如何设计的也不一样。
这里以 Chrome浏览器 为例子先行阐述内存分配,再讲如何优化递归。

1、Chrome浏览器 内存情况

在Chrome浏览器中,其 js 的内存分布形式主要分类两类call stackmemory heap

1.1 Call stack(调用栈)

Call stack 调用栈的基本概念
call stack调用栈在浏览器中的资源是有限的,是一开始就被分配好的,一般没有多大。
在Chrome浏览器中只有几M乃至更小,该栈可以保存:

  1. 基本类型的变量(相同的变量的值,如a=2,b=2,它们是独立的,并不是共享一个2)
  2. 引用类型指针(对象、函数)
  3. 函数调用情况(非定义的指针),即一个函数及其参数情况(称之为一个栈帧call frame),可以说是函数调用时的上下文

1.2 Memory heap(内存堆)

这个,由需要定义的引用类型的大小决定,类似C语言的申请内存
函数、对象等引用类型都是保存在此处,Call stack 中保存的是指向内存堆中的指针
不清楚是否有最大限制,应该取决于电脑的内存
这个不是重点,不讲

2、递归优化

2.1 问题重现

举个典型的递归的例子:

function acc(num) {
    if (num === 1) {
        return 1
    }
    return num + acc(num - 1)
    // 注意这里,实际上会先执行 acc(num - 1)。而为了保存这个num,会形成一个栈帧进入 call stack
    // 等 acc(num - 1) 执行完毕,返回一个值 ans,此时 call stack 弹出一个栈帧
    // 处理器根据该栈帧,知道要把 num 加上 ans,然后再返回出去,交给调用者
}

一般的递归,在结束的时候,会往call stack入栈,保存此时的上下文,也就是函数环境(要是学过操作系统,你应该懂上下文。这里函数上下文就是函数名称、参数、返回值等等)
如果你的递归很长,那么就会往call stack反复入栈
而栈是有大小的,超出就会报错

2.2 递归优化--尾递归

在很多编程语言中,为了防止过度递归造成栈溢出,都采用了尾递归的优化方案
尾递归的简单理解就是:

某个函数的返回语句,其语句只包含函数调用,而不包含除函数外的其他操作。

2.1 的示例结尾就不是尾递归:

  return num + acc(num - 1) // 不是尾递归,返回的语句 num + acc(num - 1) 含有加法运算

把上面的改成尾递归:

  return acc(num - 1) // 是尾递归,返回的语句只包含函数调用

尾递归优化原理主要是不需要保存其他的情况,如2.1的例子就需要保存 num 的值。
因为不需要保存其他情况,调用 acc 函数的时候,就直接执行返回的函数,acc 本身形成的栈帧就会被释放,返回的函数执行的时候就重新入帧

参考

知乎:什么是尾递归?
阮一峰:尾调用优化

posted @ 2020-12-21 17:13  Sebastian·S·Pan  阅读(231)  评论(0编辑  收藏  举报