在深入探讨前端尾递归前,我们先来了解递归和尾调用两个概念
递归
在函数内部调用自身,一般来说递归有两个状态
- 递归状态(继续递归)
- 最终状态(终止递归)
递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归的作用可以完全取代循环,因此在很多函数编程语言(如Scheme)中习惯用递归来实现循环。(递归算法)
一般而言,递归使函数看起来更加简洁,但相对循环来说不易理解。
在JavaScript语言中,函数中调用函数会形成调用栈(call stack),栈是一后进先出的结构,当函数调用链足够长时,会发生栈溢出,程序会报错,因此在使用递归时,要注意溢出的情况。
递归 | 循环 |
---|---|
程序简洁 | 易阅读 |
栈溢出 | 不会发生溢出 |
尾调用
在讨论尾调用前,我们先强调一点,尾调用的局限场景
- safari浏览器实现了对尾调用的支持
- 严格模式下生效
含义
函数最后一步是调用一个函数,最后一步并不指最后一行。可简单归纳为return + fn()
function bar(){}
function foo1() {
bar()
}
// foo1不是尾调用,实际等价于
function foo1() {
bar()
return undefined
}
// foo2不是未调用,因为最后一步不是调用一个函数,而是 bar() + 1 求和
function foo2() {
return bar() + 1
}
// 是尾调用
function foo3(num) {
switch(num) {
case 1:
return bar()
case 2:
return bar()
}
}
尾调用由于最后一步是调用一个函数,所以外部函数的调用位置,内部变量不需要用到,因此不要保存外部函数的调用帧,因此执行到尾调用函数的最后一步时,内存函数的调用帧取代最外层函数的调用帧。不会发生调用栈溢出。
尾递归
尾递归,递归结合尾调用,或者说尾调用的函数是自身。
尾递归优化
前提条件
- 严格模式
- Safari浏览器
尾递归溢出
function sum(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000);
尾调用优化
蹦床函数
封装一个蹦床函数解决调用栈溢出
function trampoline(f) {
while(typeof f == 'function') {
f = f()
}
return f
}
trampoline
函数接收一个函数,将递归改造成循环,我们将sum函数进行改造,返回sum函数的一个副本。
function sum(x, y) {
if (y > 0) {
return sum.bind(null,x + 1, y - 1); // 这里可以不使用bind,直接返回一个函数,函数内部调用sum方法
} else {
return x;
}
}
// 不使用bind
trampoline(sum(1, 100000)) // 100001
为了解决调用栈溢出的问题,我们封装了一个
trampoline
函数,另外对sum函数进行了改造。
tco函数
function tco(f) {
let value,
actived = false,
accumulated = [];
return function () {
accumulated.push(arguments);
if (!actived) {
actived = true;
while (accumulated.length > 0) {
value = f.apply(null, accumulated.shift());
}
actived = false;
return value;
}
};
}
var sum = tco(function (x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
});
巧妙利用accumulated,循环不会终止。每次执行到apply时,accumulated中会push进下一次参数,继续while循环。tco函数也使用了闭包。
总结
本文主要讨论了以下几个问题
1)前端中的尾递归,介绍了递归,尾调用,和尾递归三个基本概念。
2) 说明了尾调用的优化需要在严格模式下才会生效,尾调用优化在safari浏览器中得到实现。
3)递归可能造成调用栈溢出
4)可以使用循环解决调用栈溢出问题
5)封装了trampoline函数和tco函数,这两个函数中巧妙利用循环解决了调用栈溢出的问题。
参考:尾递归优化的实现