第六章 递归

本章是一个过渡性章节,旨在平滑地从思考函数转向更深层次的函数式风格的思考。

6.1 自吸收函数(调用自己的函数)

从历史上看,递归和函数式编程有着不可不说的渊源,或者说,它们总是被一起介绍。但我们说,理解递归更有主语理解函数式编程,原因有三:

  1. 递归的解决方案包括使用对一个普通问题子集的单一抽象的使用。

  2. 递归可以隐藏可变状态

  3. 递归是一种实现懒惰和无限大结构的方法。

我们来看一个描述,并区分它们的不同之处:

假如你考虑一个函数,比如 myLength 接收一个数组,并返回它的长度,那么它的描述如下:

  1. 从零开始计算数组大小。
  1. 遍历数组,每遍历一个元素,数组大小(size)加 1。
  2. 遍历到数组结尾,那么数组的大小(size)就是它的长度(length)。

那么我们来看看递归的思路是什么样的:

  1. 如果数组是空的,那么应当返回 0。
  1. 对数组的剩余部分,添加一个结果到 myLength。

也就是:

function myLength(array) {

 if(_.isEmpty(array)) return 0;
 
 return 1 + myLength(_.rest(array))
}

我们这里不考虑那两个 lodash 中的方法,我们要理解它的思路是什么样的,当然,如果你说我们直接使用 array.length ,那我也无话可说了。
接着我们考虑这么一个情况,实现一个函数 zip,输出的结果为:

zip(['a', 'b', 'c'], [1, 2, 3]) ==> [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]

一般来说,看过 lodash
的源码后,都会写出这么一个函数:

function zip(array1, array2) {

 const length1 = array1.length;
 const length2 = array2.length;
 if(length1 !== length2) return;
​
 const result = [];
 let i = -1;
​
 while (++i < length1) {
   result[i] = [array1[i], array2[i]];
 }
​
 return result;
}

我们一般的思路就是采用循环来处理它,那么现在,我们要使用递归呢?

显然,递归不是用来操作这样的数的。

还记得阶乘吗?也就是 0! = 1,2! = 1 * 2,n! = 1 * 2 * 3 * ··· * n

递归是这样来用的:

function fact(n) {

 return n <= 0 ? 1 : n * fact(n - 1);
}
​
fact(5); // 120

注意到了吗?递归的使用场景是在当你可以调用函数自身在做输出的时候,才需要使用递归。

注意:在使用递归的时候,一定要注意设置停止条件,否则就会造成栈溢出。

这里再提到一个概念 —— 尾递归。

尾递归是指当对任何元素进行递归调用时,该函数的最后一个动作是递归调用,那么就称其为尾递归。

感兴趣的话,就自己查询下。

递归常常用于对象和数组中,这一点在 lodash 中得以体现。

可以看这里: Github

但是,我们更多将递归看作是底层操作。也就是说,在没有更好的办法时,应该减少递归的使用量,因为 JavaScript 引擎优化的问题,也是因为栈溢出的问题,也是因为它有时不是那么容易理解等问题。

总结

本节讲到了递归,也就是函数间接或直接的调用自己。

自递归调用是搜索以及处理嵌套数据结构的强大工具,但还是这句话,谨慎使用递归 ,因为递归有时候没有高阶函数来的那么直接。

我们目前普遍的认识还是使用函数组合,仅当需要的时候才使用递归这一技术操作。

推荐阅读更多精彩内容