Javascript调用堆栈、尾部递归和手动优化的详细说明

Javascript调用堆栈、尾部递归和手动优化的详细说明
调用堆栈(调用堆栈)

调用堆栈(调用堆栈)是一个基本的计算机概念,其中引入了一个概念:堆栈帧。

堆栈帧是堆栈空间的一部分,分别分配给函数调用。
运行程序从当前函数调用另一个函数时,它将为下一个函数创建一个新的堆栈框架,并进入堆栈框架。堆栈帧称为当前帧,原始函数也有一个称为调用帧的对应堆栈帧,每个堆栈帧将存储在当前函数的局部变量中。
当函数被调用时,它被添加到调用堆栈的顶部,调用结束后,函数从调用堆栈的顶部被移除。运行正确的程序(帧指针)被传递到堆栈顶部的堆栈帧中。

在Javascript中,它是通过console.trace看到当前函数的调用框架非常方便()方法
尾调用

在尾递归之前必须知道尾调用是什么,简单地说,函数的最后一步是调用和返回另一个函数。

以下是正确的演示:
尾调用正确演示1
函数f(x){
返回g(x);
}

尾调用正确演示2
函数f(x){
如果(x > 0){
返回m(x)
}
返回n(x);
}

1个程序的最后一步是执行函数g,并将其返回到值。在2中,尾部调用不需要写在最后一行上。只要它被执行,它就是最后一步。

以下是虚假演示:
尾调用错误模型1
函数f(x){
设y=g(x);
回到Y;
}

尾调用错误模型2
函数f(x){
返回g(x)+ 1;
}
尾调用错误模型3
函数f(x){
g(x);这个步骤相当于未定义的g(x)返回
}

1最后一步是赋值操作,最后2步是加法操作,3个隐式语句返回未定义。

尾部调用优化

在调用堆栈的一部分,我们知道,当一个函数调用另一个函数的一个B,这将形成一个同时存在称为帧和当前帧B调用堆栈中的栈帧,这是因为当B完成,还需要进行正确的返回,然后一个内部变量的数量的功能位置等信息调用B函数必须否则保存在调用框架,当函数B的函数执行的继续执行,将是一个烂摊子。
现在,我们把函数B放在函数A的最后一步(即尾部调用)中,是否需要保持函数A的堆栈框架当然不是,因为那将不被重用的呼叫位置和内部变量,函数B栈帧是直接由课程A的栈帧所取代,如果内部函数使用外部函数的变量,那么该函数的栈帧还需要保留,一个典型的例子是关闭的。

网上有很多博客文章,其中有一篇广为流传,我不太同意。
函数f(){
设m=1;
设n=2;
返回g(m + n);
}
(f);
等效
函数f(){
返回g(3);
}
(f);
等效
g(3);
以下是博客原文:在上面的代码中,如果函数G不叫尾巴,函数f需要保存的内部变量m值的信息和N,对G的电话的位置,等等。但在调用g,f完成,所以执行的最后一步,我们可以删除通话记录(F),只保留G的通话记录(3)。
但我第一个想到的是执行m + n操作第一,然后函数返回在同一时间。这应该是一个叫尾。同时,M + N的值也是通过函数在函数内部,并没有直接引用,因此不能说在F变量的值需要被拯救。

一般来说,如果所有函数都称为尾部调用,那么调用堆栈的长度就会小得多,因此所需的内存量将大大减少,这就是尾部调用优化的含义。

尾递归

递归指的是在函数定义中使用函数本身的方法。函数调用本身称为递归,然后函数在结束时调用它自己,称为尾部递归。

最常见的递归斐波那契数列,递归法:
函数f(n){
如果(n 0 = = = = = | | = 1)返回n
否则返回f(n - 1)+ f(n - 2)
}
这种写作方式简单粗暴,但有一个严重的问题。调用堆栈随n呈线性增加,当N是一个大数目(我测量了一下,当N为100,浏览器窗口会死。)当时,堆栈是爆炸(栈溢出、堆溢出)。这是因为在这个递归操作,大量的堆栈帧同时保存和调用堆栈很长,会消耗大量的内存。

接下来,一般递归被升级为尾递归。
功能ftail(n,a = 0,B = 1){
如果(n=0)返回
返回ftail(n - 1,B,A + B)
}
很明显,它的调用堆栈是

复制代码代码如下所示:

FTail(5)(4, 1, 1)= = ftail ftail(3, 1, 2)=(2, 2, 3)= ftail ftail(1, 3, 5)=(0, 5, 8)= 5的回报ftail
递归重写后的调用堆栈总是更新当前堆栈帧,这完全避免了堆栈的危险。

但是思路不错,从尾部到尾部优化的起点到尾部递归是正确的,但鸡蛋:)让我们来看看V8引擎的官方团队的解释。

适当的尾部调用已实现,但尚未发货,因为
这意味着人们已经做到了,但它不能用于你:嘿,好空气。
当然,他一定有一个很好的理由。

在引擎级消除尾部递归是一种隐式行为。当程序员编写代码时,他们可能没有意识到他们已经编写了死后的尾循环递归,他们不会在死后报告堆栈溢出错误。
在优化过程中,栈信息会丢失,开发人员很难调试。
我知道所有的真相,但我不相信邪恶,我把Nodejs(v6.9.5)手动测试:
好的,我买了。

手动优化

虽然我们不能用6尾递归高端优化一时间,递归优化的实质是减少调用堆栈和避免过度记忆和爆炸叠加的风险。作为一个流行的说法,所有的功能,可以用递归可以写周期。Niklas的夏天,如果我们把递归循环,我们不会解决调用栈的问题。

计划1:直接更改内部功能,循环执行
功能软区(N,A = 0,B = 1){
而(n){
{ A,B,},A,B,}
}
返回一个
}
这个解决方案简单而粗糙,缺点是没有递归更容易理解。

方案二:蹦床(蹦床功能)
功能蹦床(f){
而(F是函数){
f=f()
}
返回F
}

函数f(n,a=0,b=1){
如果(n = 0){
{ A,B,},A,B,}
返回f.bind(null,n - 1,A,B)
{人}
返回一个
}
}

蹦床(F(5))返回5
这种写作方式很容易理解,也就是说,蹦床的功能需要仔细研究,缺点是需要修改原功能的内部书写。

方案三:尾部递归函数循环法
功能tailcalloptimize(F){
让价值
激活=假
常量累积= {
返回函数累加器(){
Accumulated.push(参数
如果(!主动){
主动=真
而(累计,长度){
价值= f.apply(这accumulated.shift())
}
主动= false
返回值
}
}
}

const f = tailcalloptimize(函数(n,a = 0,B = 1){)
如果(n=0)返回
返回f(n - 1,b,a + b)
})
f(5)返回5
一个新的功能,蓄能器,是tailcalloptimize包装后返回,这实际上是执行当f是执行。这种方法不能修改原始递归函数和递归调用的问题是可以解决的方法只有当使用这个方法递归叫做。

总结

尾递归优化是一件好事,但现在我们不能使用它,我们应该非常敏感的地方,我们使用递归在正常的编码过程中,并避免在任何时候死循环和爆炸堆栈的危险。毕竟,良好的工具不如良好的习惯。
以上是本文的全部内容,希望能对您有所帮助,希望大家多多支持
免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部