为什么要优化递归?

1、 递归的定义
按照 Wiki 百科的描述:
在数学和计算机科学中,递归指由一种(或多种)简单的基本情况定义的一类对象或方法,并规定其他所有情况都能被还原为其基本情况。
一个很常见的例子是计算斐波那契数列:
function Fab(n) {
    if (n < 2) return 1;
    return Fab(n-1) + Fab(n-2);
}
需要注意的是,对于计算机科学来说,狭义的 “递归”,应当是在函数定义中,引用该函数自身的定义。
例如下面的代码不应视作递归,因为其未使用函数自身的定义
function Hutia(){
    alert(String(Hutia));
}
2.为什么递归会造成堆栈溢出
 在 OS / 浏览器 / ... 等平台运行程序的时候,每一次对函数的调用前,都会将当前的运行环境保存起来,以便于函数调用完毕后,恢复现场。这些数据,通常保存在堆栈里。一般在具体实现的时候,这个空间并不是无限的。也正是因此,函数的最大调用深度也是有限的。
附上网上随便找的一个,关于最大调用深度的测试结果:各个浏览器调用堆栈(Call Stack)的最大深度 « 笨笨剥壳
由于递归函数的特点,导致如果边界检查存在缺陷,那么就可能导致超过这个最大深度,从而超出堆栈的存储能力,也就是一般所说的“溢出”。
理论上来说,如果够无聊的话,手工书写 “一层套一层” 的函数调用代码,也是有可能做到堆栈溢出的。不过按上面链接中的那个测试结果来说,需要写几千次甚至上万次罢了。

3、 为什么要优化递归
递归可以带来清晰、明确的代码表述,因此很多程序设计语言都保留了这个特性。但是由于保存运行环境,并为函数调用准备好入口的上下文,这些操作会带来额外的开销,因此在编译器不够给力的情况下,可以考虑将递归转换为循环,来优化性能。


4、 JavaScript 中,回调 与递归的区别
wiki 百科中关于回调的定义也很明确:
回调函数
在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用。

注意是 :“某一块可执行代码的引用”。

因此,在 JavaScript 中,由于其 “事件驱动” (Event-Driven)的特性,使用像 "setTimeout"、 “nextTick” 等方式对指定函数的调用,实际上是将该函数的引用(指针)储存起来,并在适当的时候调用。
换句话说,JavaScript 中, "setTimeout"、 “nextTick” 等方式调用的函数,并不会形成类似于递归那样, “一层套一层” 的调用链。下一次函数调用时,上一个 “父” 函数的调用已经执行完毕。也就不存在堆栈溢出的风险了。


说了这么多,并不是为了学究般的区分 “回调” 与 “递归” 两个词语的差异。而是希望初学者能够理解程序执行背后的逻辑,少走一些弯路。


作者:tia hu
链接:https://www.zhihu.com/question/30078697/answer/46681063
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


原文地址:https://www.cnblogs.com/qianxinxu/p/6497611.html