JS函数进阶相关,为 尾调用
尾回调
蹦床函数
做个笔记
具体的可以看 阮一峰 ECMAScript 6 入门 函数的扩展
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x);
}
// 或者这样的
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
以上的示例,都不属于
尾调用
return
调用函数。只能调用函数,不能在进行其他的处理,就像情况2一样还需要在进行处理的函数(以下的结论引用于 阮一峰 ECMAScript 6 入门 函数的扩展 )
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
总之:就是减少页面的调用帧
. 避免 调用栈
溢出。
有了上面 尾调用
的初步认识,尾递归其实就是 尾回调
+ 递归
的结合体
众所周知递归的 调用栈
是很可怕的。甚至一不小心就会 栈溢出
。
我们可以准备个经典的递归案例:斐波拉契数列
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2); // 这里返回了2个函数,并且都进行了加法运算,不属于尾调用
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
chrome浏览器在运行 100
或者以上的时候,会页面崩溃。其实就是 堆栈溢出
的体现,就算没有溢出,相信也已经运行了非常久才出的结果
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2); // 直接返回一个函数,属于尾调用
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
优化后,可以做到秒出结果。这也就是 尾调用
的魅力所在
然而。尾调用和尾递归,需要在严格模式
下才能使用
可以在方法开始的地方,加上 'use strict';
如果因为历史原因(项目过大,时间太久远,不能贸然使用严格模式)。那还是有办法的,就是下面要介绍的 蹦床函数
蹦床函数(trampoline)就是将 递归执行
转为 循环执行
。
执行的都是同样的步骤,只是反复执行,就好像在蹦床,跳上去,掉下来,在跳上去…
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题
// 蹦床函数
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
// 递归函数 参数x是需要累加的值,参数y控制递归次数。
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
// 使用蹦床函数调用递归函数
trampoline(sum(1, 100000))
细心的可能会发现,为啥 12 行会有一个
bind
函数,为啥不用apply
和call
。
因为bind和他们最大的不同就是bind
不会立刻执行,而是返回一个函数
。 返回函数这个很重要,这样才能用于蹦床函数。
sum方法也因为这个 bind
每一步返回另一个函数
emmm… 这样蹦床函数用起来,好像又偏复杂了。还得把原先的递归函数改成返回另外一个函数,看着有点绕
然而。蹦床函数并不是尾递归优化的最终版本
下面的实例才是:
function tco(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 sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
// 100001
tco函数是尾递归优化的实现
它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。
搞了那么多,都是为了一个优化,加上es6的普及和浏览器的支持,明明一句 'use strict';
可以解决的问题,又何必写那么多复杂的函数。
不过这些优化可以用作底层原理的理解还是很不错的。