JS的递归与TCO尾调用优化

转自:http://www.javashuo.com/article/p-osnrvzqn-dd.htmljavascript

这两天搜了下JS递归的相关文章, 以为这篇文章很不错, 就顺手翻译了下,也算给本身作个笔记,题目是我本身加的。原文很长,写得也很详尽,这里并不是逐字翻译, 而是做者所讲的主要概念加上我本身的一些理解,本文中解决方案的实际意义并非特别大,但算法的逻辑挺有意思,不过也略抽象,理解须要花点时间(囧,估计我太闲了) 文中的用例🌰所有来自原文:html

原文连接:(原题为:理解JS函数式编程中的递归)
Understanding recursion in functional JavaScript programmingjava

递归存在的问题

在JS的递归调用中,JS引擎将为每次递归开辟一段内存用以储存递归截止前的数据,这些内存的数据结构以“栈”的形式存储,这种方式开销很是大,而且通常浏览器可用的内存很是有限。下面这个函数使用递归的方式求和:算法

//使用递归将求和过程复杂化
function sum(x, y) {
    if (y > 0) {
      return sum(x + 1, y - 1);
    } else {
      return x;
    }
}

sum(1, 10); // => 11

当运算规模较小时,这种方式能够正常输出结果,但是当把参数变为sum(1,100000)时,就会形成“栈溢出错误(stack overflow 这可不是那个问答网站哦)”浏览器就会报错Uncaught RangeError: Maximum call stack size exceeded编程

尾调用优化 Tail Call Optimisation

在有些语言中,执行尾递归时将会被自动识别,继而在运行时优化成循环的形式,这种优化逻辑大可能是Tail Call Optimisation尾部调用优化,(尾调用概念就是在函数最后一步调用其余函数,尾递归即在最后一步调用自身)关于尾递归与尾调优化更详细的概念解读能够看下阮一峰的这篇文章👉 尾调用优化 (也就是说执行尾递归时,程序无须储存以前调用栈的值,直接在最后一次递归中输出函数运算结果,这样就大大节省了内存,而这种优化逻辑就是在代码执行的时候将其转换为循环的形式)
另外在Babel的说明文档中也提到了尾调用👉 BABEL Tail Callssegmentfault

以上的sum函数, 使用尾递归,将是这个样子:数组

function sum(x, y) {
    function recur(a, b) {
        if (b > 0) {
            return recur(a + 1, b - 1);
        } else {
            return a;
        }
    }
//尾递归即在程序尾部调用自身,注意这里没有其余的运算
    return recur(x, y);
}

sum(1, 10); // => 11

以上这种写法在有TCO机制的语言中将在执行时内部优化成循环形式而不会产生“栈溢出”错误,注意,在当前版本的JS中以上写法是无效的!由于在当前广泛的JS版本(ES5)中并无这个优化机制。可是在ES6中已经实现了这个机制 在当前广泛的JS版本中咱们只能使用替代方案。浏览器

这里插一句:使用Babel能够在当前JS版本中用ES6的特性(Babel能够将使用ES6特性编程的代码转换成兼容的ES5形式),将原sum()函数输入Babel的编译器后,确实被转换成了循环的形式,感兴趣的同窗能够本身试试:
BABEL编译器转换sum()函数的结果以下(对于算法逻辑不太感兴趣的同窗看到这里就差很少了,
能够直接将一些深递归放到Babel中转换下就能够了):babel

var _again = true;

  _function: while (_again) {
    var x = _x,
        y = _x2;
    _again = false;

    if (y > 0) {
      _x = x + 1;
      _x2 = y - 1;
      _again = true;
      continue _function;
    } else {
      return x;
    }   } } 

替代方案

在当前的JS版本(ES5)中可使用如下方式来优化递归。咱们能够定义一个Trampolining(蹦床)函数来解决参数过大形成的“栈溢出”问题。数据结构

    //放入trampoline中的函数将被转换为函数的输出结果
function trampoline(f) {
    while (f && f instanceof Function) {
        f = f();
    }
    return f;
}

function sum(x, y) {
    function recur(x, y) {
        if (y > 0) {
          return recur.bind(null, x + 1, y - 1);
        } else {
          return x;
        }
    }
//
    return trampoline(recur.bind(null, x, y));
}

sum(1, 10); // => 11

在以上的方案中, trampoline函数接受一个函数做为参数,若是参数是函数就被执行后返回,若是参数不是函数将被直接返回,嵌套函数recur中,当y>0时返回一个参数更新了的函数,这个函数被转入trampoline中循环,直到recur返回xx不是函数因而在trampoline中被直接返回。原文中做者对于每一步都有详尽的解释, 感兴趣的同窗建议能够去看看原文。简单地说:以上逻辑就是将递归变成一个条件, 而外层trampoline函数执行这个条件判断并循环。好吧,接下来更绕的来了-_-#

以上这种方法虽然解决了大参数递归的问题,可是却须要将代码转换成trampoline的模式,比较不灵活, 下面做者介绍了一种更灵活方便的方案。

更好的方案

做者在此警告:前方高能, 该方法不须要改动源码,可是略抽象,理解可能须要花点时间。

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;
        }
    }
}
//这种方式确实有点奇怪,但的确没有改动不少源码,只是以直接量的形式使用tco函数包裹源码
var sum = tco(function(x, y) {
    if (y > 0) {
      return sum(x + 1, y - 1)
    }
    else {
      return x
    }
});
sum(1, 10) // => 11
sum(1, 100000) // => 100001 没有形成栈溢出
  • 首先以函数表达式的形式将tco函数的返回值赋给sum,tco函数的返回值是accumulator函数,也就是说当执行sum(1,10)的时候便是在执行accumulator(1,10),牢记这点对后续理解颇有帮助。

  • accumulator是个闭包,这意味着能够访问在tco中定义的valueactive以及accumulated

  • 前面已经讲了,当咱们执行sum的时候至关因而执行accumulator,因而accumulator 将实参传入accumulated数组,好比执行sum(1,10)那么这里传入的就是类数组对象[1,10],accumulated如今就是一个length为1的二维数组。

  • 进入while循环,这里是关键:value = f.apply(this, accumulated.shift()); 在这条语句中, f表示外包的匿名函数,它判断y的值后返回一个sum (这里很容易产生混乱,若是咱们忽略while循环中的细节,很容易将其误认为也是递归)

  • 匿名函数f判断y的值返回一个sumsum的参数被改变了,前面提到执行sum至关于执行accumulator,因而新的参数被加入到了accumulator可是由于这时active的值依然是true(由于如今执行流还在while循环里),因此执行这个被返回的sum就会获得一个undefined的值,value被赋值为undefined

  • 但是由于执行了被返回的sum(也就是执行了accumulator)尽管没有进入if(!active),可是执行了第一条语句,因此accumulated被从新push进了在外包的匿名函数中被修改的实参,因此while循环继续(理解这里是关键)。

  • while循环一直执行到accumulated中的值为空, 在value = f.apply(this, accumulated.shift()); 每次return一次sum后accumulated 都会被从新推入一个实参(accumulated的length始终为1),直到匿名的外包函数return出x,因而x的值被赋给value被返回出来。

注意:以上主要仍是根据我本身的理解来阐述逻辑, 确实比较绕,做者原文写得更加详细

总结

以上方法就是在不改动源码的状况下实现的TCO优化, 做者在该文章的Update中介绍了另外的非TCO的优化递归的方法,不过篇幅有限就再也不贴出来了,就我自身感受而言,若是对算法的逻辑实现不感兴趣, 大能够直接用Babel将深递归转换成优化后的形式。另外这也有一篇介绍JS中递归与循环的的文章,其中也有TCO优化的相关介绍:
👉Recursion in Functional JavaScript

感受以上代码的实际意义可能并无那么大, 为了写这篇博客也是耗了我一天,囧rz,但也挺佩服这哥们:“我靠,这也能想获得!”

相关文章
相关标签/搜索