JS 递归调用溢出
前言
本来我是用js编程一道题,使用了递归,结果浏览器报错RangeError: Maximum call stack size exceeded
。
意思也就是最大的调用栈规格超出了,我隐隐知道是怎么回事了,估计是存放 call 的 stack 容量不够了。
这涉及到浏览器对 js 的内存分配情况了,每个浏览器对各自对 js 实现方式不一样,js 内存分布如何设计的也不一样。
这里以 Chrome浏览器 为例子先行阐述内存分配,再讲如何优化递归。
1、Chrome浏览器 内存情况
在Chrome浏览器中,其 js 的内存分布形式主要分类两类call stack
和memory heap
。
1.1 Call stack(调用栈)
Call stack 调用栈的基本概念
call stack
调用栈在浏览器中的资源是有限的,是一开始就被分配好的,一般没有多大。
在Chrome浏览器中只有几M乃至更小,该栈可以保存:
- 基本类型的变量(相同的变量的值,如
a=2,b=2
,它们是独立的,并不是共享一个2) - 引用类型指针(对象、函数)
- 函数调用情况(非定义的指针),即一个函数及其参数情况(称之为一个栈帧
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 本身形成的栈帧就会被释放,返回的函数执行的时候就重新入帧