递归函数就是会直接或者间接地调用自身的一种函数。递归是一种强大的编程技术,它把一问题分解为一组相似的子问题,每一个都用一个寻常解去解决。一般来说,一个递归函数调用自身去解决它的子问题。

书上的一个小例子“汉诺塔”。塔上有3根柱子和一个套直径都不同的空心圆盘。开始时源柱子的所有圆盘都按照从小到大的顺序堆叠。目标是每次移动一个圆盘到另一根柱子,最终把一堆圆盘移动到目标柱子上,期间不允许把较大的圆盘放在较小的圆盘上。

 var hanoi = function(disc, src, aux, dst){
    if (disc > 0) {
        hanoi(disc - 1, src, dst, aux);
        document.writeln('Move disc' + disc + ' form ' + src + ' to ' + dst);
        hanoi(disc - 1, aux, src, dst);
    }
};

hanoi(3, 'src', 'aux', 'dst');

运行代码结果:

Move disc1 form src to dst
Move disc2 form src to aux
Move disc1 form dst to aux
Move disc3 form src to dst
Move disc1 form aux to src
Move disc2 form aux to dst
Move disc1 form src to dst
  • hanoi函数把一堆圆盘从一根柱子移动到另外一根柱子,必要使用辅助的柱子。它把该问题拆分成3个小问题。它先移动到一对圆盘中较小的圆盘到辅助柱子上,从而露出下面较大的圆盘,然后移动下面的圆盘到目标柱子上。最后,他将刚才较小的圆盘从辅助柱子上再移动到目标柱子上。通过递推地调用自身去处理一堆圆盘的移动,从而解决那些问题。
  • 传递给hanoi函数的参数包括当前移动的圆盘的编号和它将要用到的3根柱子。当它调用自身的时候,它去处理当前正在处理的圆盘之上的圆盘。最终,他会一个不存在的圆盘编号去调用。在这样的情况下,他不执行任何操作。由于该函数对非法只不理会,也不用担心死循环的问题。

书上第二个例子是说递归函数可以非常高效率的操作树形结构,比如DOM。每次递归调用是处理指定的树的一小段。

// 它从某个指定的节点开始,按照 HTML 源码中的顺序访问该数的每个节点。
var walk_the_DOM = function(node, func){
    func(node);
    node = node.firstChild;
    while (node) {
        walk_the_DOM(node, func);
        node = node.nextSibling;
    }
};

// 它调用 walk_the_DOM 函数,传递一个用来查找节点属性名的函数为参数。
// 匹配的节点会累加到一个结果数组里面。
var getElementsByAttribute = function(att, val){
    var results = [];

    walk_the_DOM(document.body, function(node){
        var actual = node.nodeType === 1 && node.getAttribute(att);
        if (typeof actual === 'string' && (actual === val || typeof value 
            != 'string')) {
            results.push(node);
        }
    });

    return results;
};

有一些语言提供了尾递归的优化。这意味着如果一个函数返回自身递归的结果,那么调用的过程会被替换成以循环,可以提高速度。可惜js并没有尾递归的优化。

好运的是,ES6给我们带来了尾递归,详细迎接ECMAScript 6, 使用尾递归
拓展:什么情况下用递归?


_我已经从中二毕业了
7.9k 声望235 粉丝

不搞前端会死星人。