性能提升:你不知道的那些递归冷知识

158 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天。

递归,我们都知道,它可以把复杂的算法简单化但是关于递归周边的一些冷知识你未必知道哦。知道这些冷知识,关于性能优化,你就又提升了一大截。

调用栈限制

JavaScript 引擎支持的递归数量始于调用栈大小直接相关的,所有浏览器(IE除外)都有固定数量的调用栈限制,大多数现代浏览器的调用栈数量比老版本多很多,大概展示如图:

图片.png

当超出这个限制的时候浏览器会报异常错误,可以通过 try-catch 表达式捕获:

IE: "Stack overflow at line x"
Firefox:"too much recursion"
Safari: "Maximum call stack size exceeded"
Oprea: "Abort(control stack overflow)"
Chrome: 唯一一个不显示调用栈溢出错误的浏览器

这些报错是不是看着有些眼熟,恍然大悟了吧!!!

更有意思的是具体的异常类型是因浏览器不同而不同的,具体我们可以不了解,但是有一点需要清楚: 如果不捕获这些异常,他们会想其他错误一样向上冒泡传递,在 Firefox 中冒泡止于 Firebug 和错误控制台,在 Safari/Chrome 中错误会显示在 JavaScript 控制台上。

递归模式

当你遇到调用栈限制的时候,第一步要检查代码中的递归实例,大多数调用栈错误与以下两种递归模式相关:

(1)直接递归模式

function recurse(){
    recurse()
}
recurse()

这种比较常见,还是比较容易定位的。

(2)隐伏模式

function first(){
    second()
}

function second(){
    first()
}

first()

这种模式两个函数相互调用,形成一个无限循环,并且很难定位原因。

定位递归问题的一般步骤:

(1)最常见的导致栈溢出的原因是不正确的终止条件,所以定位错误的第一步是验证终止条件; (2)当确认终止条件没问题的时候,再检查算法中是不是包含了多层递归,建议使用迭代、Memoization 或两种结合使用。

接下来我们说说迭代与 Memoization.

迭代

迭代算法通常包含几个不同的循环,分别对应计算过程的不同方面,任何递归能实现的算法都可以用迭代来实现。

这里我们需要知道一点:用循环替代长时间运行的递归函数时可以提升性能的,因为运行一个循环比反复调用一个函数的开销要少的多。

Memoization

Memoization 又称为 Tabulation,是一种主要用于加速程序计算的优化技术,它是的函数避免重复计算已被处理过的参数,而返回已缓存的结果。简单来说就是将函数返回值缓存起来的方法,原理是把函数的每次执行结果都放入一个键值对(数组也可以,视情况而定)中,在接下来的执行中,在键值对中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才 真正执行函数体的求值部分。很明显,找值,尤其是在键值对中找值,比执行函数快多了。