在深入探讨前端尾递归前,我们先来了解递归和尾调用两个概念
在函数内部调用自身,一般来说递归有两个状态
递归式方法可以被用于解决很多的计算机科学问题,因此它是计算机科学中十分重要的一个概念。绝大多数编程语言支持函数的自调用,在这些语言中函数可以通过调用自身来进行递归。计算理论可以证明递归的作用可以完全取代循环,因此在很多函数编程语言(如Scheme)中习惯用递归来实现循环。(递归算法)
一般而言,递归使函数看起来更加简洁,但相对循环来说不易理解。
在JavaScript语言中,函数中调用函数会形成调用栈(call stack),栈是一后进先出的结构,当函数调用链足够长时,会发生栈溢出,程序会报错,因此在使用递归时,要注意溢出的情况。
递归 | 循环 |
---|---|
程序简洁 | 易阅读 |
栈溢出 | 不会发生溢出 |
在讨论尾调用前,我们先强调一点,尾调用的局限场景
含义
函数最后一步是调用一个函数,最后一步并不指最后一行。可简单归纳为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()}
}
尾调用由于最后一步是调用一个函数,所以外部函数的调用位置,内部变量不需要用到,因此不要保存外部函数的调用帧,因此执行到尾调用函数的最后一步时,内存函数的调用帧取代最外层函数的调用帧。不会发生调用栈溢出。
尾递归,递归结合尾调用,或者说尾调用的函数是自身。
前提条件
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;}
}
// 不使用bindtrampoline(sum(1, 100000)) // 100001
为了解决调用栈溢出的问题,我们封装了一个
trampoline
函数,另外对sum函数进行了改造。
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函数,这两个函数中巧妙利用循环解决了调用栈溢出的问题。
参考:尾递归优化的实现
上一篇:【王道数据结构】第八章 | 排序
下一篇:BigDecimal使用注意避坑