递归条件
构成递归需要具备边界条件、递归前进段和递归返回段,当边界条件不满足时,递归前进,当边界条件满足时,递归返回。阶乘中的 n == 1 和 斐波那契数列中的 n < 2 都是边界条件。
总结下递归的特点:
- 子问题必须和原始问题有同样的事,且更为简单;
- 不能无限制地调用本身,须有一个出口,化简为非递归状况处理。
执行上下文栈
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
对阶乘函数分析执行的过程,我们发现,JavaScript 会不停的创建执行上下文压入执行上下文栈,对于内存而言,维护这么多的执行上下文也是一笔不小的开销,我们该如何优化呢?
答案就是尾调用
尾调用
尾调用,是指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。
1 | // 尾调用 |
1 | // 非尾调用 |
并不是尾调用,因为 g(x) 的返回值还需要跟 1 进行计算后, f(x) 才会返回值。
两者的区别在哪?答案就是–> 执行上下文栈的变化不一样。
这里使用一个数组来模拟执行上下文栈的行为。
1 | ECStack = []; |
1 | // 尾调用 |
1 | // 非尾调用 |
也就是说尾调用会在压入执行上下文栈后,因为原函数执行完毕而立马弹出,非尾调用则因为需要先进行运算,此时碰到了一个函数又会产生一个执行上下文,并且压入栈中,如果一直没有函数执行完,就会一直在执行上下文栈中层层压入。
函数调用自身,称之为递归,如果尾调用自身,就称之为尾递归。
我们只需要把阶乘改造成一个尾递归形式,就可以避免创建过多的执行上下文。
阶乘函数优化
1 | //原阶乘函数 |
1 | //改造后的阶乘 |
然而,当我们计算 4 的阶乘,结果函数要传入 4 和 1,我们就不能只传入一个 4 吗?
这个时候可以使用 偏函数 partial 函数了:
1 | function partial(fn) { |
递归的一些应用
- 数组扁平化
1 | function flatten(arr) { |
- 深浅拷贝
1 | var deepCopy = function(obj) { |
- 从零实现 jQuery 的 extend
1 | var class2type = {}; |
- 判断两个对象相等
1 | var toString = Object.prototype.toString; |
- 函数柯里化
1 | // 第三版 |