本章是一个过渡性章节,旨在平滑地从思考函数转向更深层次的函数式风格的思考。
6.1 自吸收函数(调用自己的函数)
从历史上看,递归和函数式编程有着不可不说的渊源,或者说,它们总是被一起介绍。但我们说,理解递归更有主语理解函数式编程,原因有三:
-
递归的解决方案包括使用对一个普通问题子集的单一抽象的使用。
-
递归可以隐藏可变状态
-
递归是一种实现懒惰和无限大结构的方法。
我们来看一个描述,并区分它们的不同之处:
假如你考虑一个函数,比如 myLength 接收一个数组,并返回它的长度,那么它的描述如下:
- 从零开始计算数组大小。
- 遍历数组,每遍历一个元素,数组大小(size)加 1。
- 遍历到数组结尾,那么数组的大小(size)就是它的长度(length)。
那么我们来看看递归的思路是什么样的:
- 如果数组是空的,那么应当返回 0。
- 对数组的剩余部分,添加一个结果到 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 引擎优化的问题,也是因为栈溢出的问题,也是因为它有时不是那么容易理解等问题。
总结
本节讲到了递归,也就是函数间接或直接的调用自己。
自递归调用是搜索以及处理嵌套数据结构的强大工具,但还是这句话,谨慎使用递归 ,因为递归有时候没有高阶函数来的那么直接。
我们目前普遍的认识还是使用函数组合,仅当需要的时候才使用递归这一技术操作。
网友评论