这篇文章运用简单易懂的例子给大家介绍如何使用ES6尾调用优化,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。
ES6包含了一个性能领域的特殊要求。这与一个涉及函数调用的特定优化形式相关:即尾调用优化(Tail Call Optimization,TCO)。简单地说,尾调用就是一个出现在另一个函数“结尾”处的函数调用。这个调用结束之后就没有其余事情要做了(除了可能要返回结果值)
什么尾调用
举个例子,下面是一个非递归的尾调用:
function foo(x) { return x } // 尾调用 function bar(y) { return foo(y + 1) } // 非尾调用 function baz() { return 1 + bar(40) } baz() // 输出42
说明: foo(y+1) 是 bar(...) 中的尾调用,因为在 foo(...) 完成后, bar(...) 也完成了,并且只需要返回 foo(...) 调用的结果。然而, bar(40) 不是尾调用,因为在它完成后,它的结果需要加上1才能由 baz() 返回。
在JavaScript里,调用一个新的函数需要额外的一块预留内容来管理调用栈,成为栈帧。所以前面的代码一般会同时需要为每个 baz() 、 bar(...) 、 foo(...) 保留一个栈帧。
然而,如果支持TCO的引擎能够意识到 foo(y+1) 调用位于尾部,这意味着 bar(...) 基本上已经完成了,那么在调用 foo(...) 时,它就不需要创建一个新的帧栈,而是可以重用已有的 bar(...) 的帧栈。这样不仅速度快,而且节省内存。
什么是尾递归
在计算机科学里,尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。这种情形下称该调用位置为尾位置。若这个函数在尾位置调用本身(或是一个尾调用本身的其他函数等等),则称这种情况为尾递归,是递归的一种特殊情形。尾调用不一定是递归调用,但是尾递归特别有用,也比较容易实现。
TCO的意义
在程序运行时,计算机会为应用程序分配一定的内存空间;应用程序则会自行分配所获得的内存空间,其中一部分被用于记录程序中正在调用的各个函数的运行情况,这就是函数的调用栈。常规的函数调用总是会在调用栈最上层添加一个新的堆栈帧(stack frame,也翻译为“栈帧”或简称为“帧”),这个过程被称作“入栈”或“压栈”(意即把新的帧压在栈顶)。当函数的调用层数非常多时,调用栈会消耗不少内存,甚至会撑爆内存空间(栈溢出),造成程序严重卡顿或意外崩溃。尾调用的调用栈则特别易于优化,从而可减少内存空间的使用,也能提高运行速度。其中,对尾递归情形的优化效果最为明显,尤其是递归算法非常复杂的情形。
在简单的代码片段中,这类优化算不了什么,但是在处理递归时,这就解决了大问题,特别是如果递归可能会导致成千上百个栈帧的时候。有了TCO,引擎可以用同一个栈帧执行所有的这类调用!
递归是 JavaScript 中一个纷繁复杂的主题。因为如果没有TCO的话,引擎需要实现一个随意的限制来界定递归栈的深度,达到了就得停止,以防止内存耗尽。有了TCO,尾调用的递归函数本质上就可以任意运行,因为再也不需要使用额外的内存,也没有了内存溢出的问题。
下面用尾递归实现一个典型的阶乘函数:
// 用循环实现 function factorial(n) { if (n<2) return 1 var res = 1 for (var i = n; i > 1; i--) { res *= i } return res } // 用尾递归实现 function factorial(n) { function fact(n, res) { if (n < 2) return res return fact(n-1, n*res) } return fact(n, 1) } factorial(5) // 输出120
注意:TCO只用于有实际的尾调用的情况,如果你写了一个没有尾递调用的函数,那么性能还是会回到普通帧栈分配的情形,引擎对这样的递归调用栈的限制也仍然有效。
关于如何使用ES6尾调用优化就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。