内容简介:尾调用(Tail Call)是函数式编程的一个概念,意思是在某个函数的最后一步调用另一个函数。在函数调用时,会在内存形成调用帧,用来保存调用位置和内部变量等信息,而嵌套函数则会形成调用栈。尾调用由于是函数的最后一步操作,因此不需要保留外层函数的调用帧,利用函数的这个特性进行优化,就是尾调用优化(Tail Call Optimization)。利用尾调用优化来优化递归函数,便是尾递归优化。因为递归函数在到达递归边界之前会不停保存调用帧,一旦递归深度过大,就容易发生栈溢出的错误,因此需要使用尾递归优化来解决这个
尾调用优化
尾调用(Tail Call)是函数式编程的一个概念,意思是在某个函数的最后一步调用另一个函数。在函数调用时,会在内存形成调用帧,用来保存调用位置和内部变量等信息,而嵌套函数则会形成调用栈。尾调用由于是函数的最后一步操作,因此不需要保留外层函数的调用帧,利用函数的这个特性进行优化,就是尾调用优化(Tail Call Optimization)。
尾递归优化
利用尾调用优化来优化递归函数,便是尾递归优化。因为递归函数在到达递归边界之前会不停保存调用帧,一旦递归深度过大,就容易发生栈溢出的错误,因此需要使用尾递归优化来解决这个问题。
递归函数改写成尾递归
以斐波那契数列求解的递归函数为例:
function fibonacci(n) { if (n <= 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(100)) // ......
此时当 n 大于 50 时运算速度极慢,大于 100 时基本无响应。使用柯里化改写该递归函数,将多参数转换为单参数形式,即可得到尾递归优化的求解函数:
function currying(f, n1, n2) { return function(m) { return f.call(this, m, n1, n2); }; } function fibonacci(n, n1, n2) { if (n <= 2) return n2; else return fibonacci(n - 1, n2, n1 + n2); } var fib = currying(fibonacci, 1, 1); fib(10000) // Infinity
在 ES6 中可以使用默认参数,因此有了最简单的实现方法:
function fibonacci(n, n1 = 1, n2 = 1) { if (n <= 2) return n2; else return fibonacci(n - 1, n2, n1 + n2); } fibonacci(10000) // Infinity
尾递归优化的实现方法
由于某些环境下无法直接使用尾调用优化,因此需要自己实现。实现原理也不复杂,就是用循环语句代替递归语句。
实现1:蹦床函数
还是以斐波那契数列为例
// 递归函数 function fibonacci(n, n1 = 1, n2 = 1) { if (n <= 2) return n2; else return fibonacci.bind(null, n - 1, n2, n1 + n2); // 返回一个新的函数 } // 蹦床函数 function trampoline(f) { while (f && f instanceof Function) f = f(); return f; } trampoline(fibonacci(10000)); // Infinity
首先将递归函数进行尾调用优化,然后在函数返回部分利用 bind()
方法返回一个全新的函数,以便蹦床函数调用。蹦床函数的核心部分是一个循环,只要传入的参数是一个函数,就会调用这个函数,直到传参不再是一个函数为止。结合起来看可以发现,蹦床函数的作用就是不断用一个新的函数替代原函数,直到遇到递归边界,返回结果。需要注意的是,蹦床函数并不是真正的尾递归优化,因为蹦床函数每次循环会产生一个新的函数帧。下面的实现才是真正的尾递归优化。
实现2:尾递归优化函数
// 尾递归优化函数 function tailCallOptimize(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); if (!active) { active = true; while (accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } }; } var fibonacci = tailCallOptimize(function(n, n1 = 1, n2 = 1) { if (n <= 2) return n2; else return fibonacci(n - 1, n2, n1 + n2); }); console.log(fibonacci(10000)); // Infinity
这个实现稍微有点复杂,要理解 tailCallOptimize()
这个函数,其关键点就在中的“active”和“accumulated”,一个用来保存函数当前状态,一个用来保存当前递归的参数。首次运行时,保存参数,“active”变为“true”,意思是此时已经进入尾递归优化阶段,之后的每次调用,都不会进入 if
语句中,仅仅保存该次调用的参数,直到最后到达递归边界,返回一个值,此时“accumulated”为空,跳出循环,恢复状态,返回结果。整个过程的核心部分只有一个函数调用帧,因此这才是真正的尾递归优化。
测试
什么时候可以直接使用尾递归优化,什么时候要自己实现,有必要做一个测试看看,测试使用的代码如下:
"ues strict" function fibonacci(n, n1 = 1, n2 = 1) { if (n <= 2) { console.trace(); // 跟踪函数调用帧 return n2; } else return fibonacci(n - 1, n2, n1 + n2); }
- Chrome 72.0.3626.121
fibonacci(5) // 以下为输出 fibonacci @ VM92:3 fibonacci @ VM92:6 fibonacci @ VM92:6 fibonacci @ VM92:6 (anonymous) @ VM131:1 5
- Firefox Firefox 65.0.2
fibonacci(5) // 以下为输出 fibonacci debugger eval code:3 fibonacci debugger eval code:6 fibonacci debugger eval code:6 fibonacci debugger eval code:6 <anonymous> debugger eval code:1 5
- Node 11.0.0
fibonacci(5) // 以下为输出 Trace at fibonacci (repl:3:13) at fibonacci (repl:6:15) at fibonacci (repl:6:15) at fibonacci (repl:6:15) at repl:1:1 at Script.runInThisContext (vm.js:119:20) at REPLServer.defaultEval (repl.js:331:29) at bound (domain.js:395:14) at REPLServer.runBound [as eval] (domain.js:408:12) at REPLServer.onLine (repl.js:644:10) 5
查了相关资料发现如下说法:“尾归调用的想法是好的,但是落地的时候出现了分歧,node在后续的版本中支持过尾归调用,但后续给去掉了。浏览器上只有safari支持,而其他浏览器上并不支持。”由此可知目前最主流的引擎都没有实现尾递归优化,如果真的需要使用,还是需要自己使用循环的方式来实现。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。