这里指的通用套路是把递归执行改为在一个函数中循环执行。出于好奇心想找出一种把递归改为非递归的通用方式,并学习其中的思路。在网上找了几篇文章,结合函数调用栈的理解,感觉自己总结的应该比较全面了,所以记录下来跟大家交流下。
先看一段简单的代码,计算阶乘的递归函数:
// n! = n * (n - 1) * (n - 2) * ... * 1
function getFactorial(n) {
if (n === 1) {
return 1;
} else {
return n * getFactorial(n - 1);
}
}
递归调用简单点来说就是函数在执行过程中调用了自身。在下一次调用中如果没达到设定的递归结束条件,这个过程会一直持续下去;当递归条件结束时,调用链条中的函数会一个接一个的返回。如以上的代码,当 n === 1 时,就会触发递归的结束条件。
这里我们思考一下,递归函数的执行跟普通的函数嵌套执行有什么不同?
其实没什么本质区别,递归调用和普通函数嵌套调用的层数一样也是有限的,层数的多少由递归结束条件来决定,无非是递归是自身调用自身(直觉上是代码的执行又回到了前面的行数)。接下我们康康函数嵌套执行时发生了什么。
如果你了解计算机执行汇编/机器码的原理就会知道我接下来可能会说运行时函数调用栈。不过我打算用一种简单的描述来引导你明白或加深印象。
首先,我先问个问题:一个函数在某一次的执行过程中,是什么东西让这一次执行与另外一次执行是有所差别的?
简单点来说,可以把这个问题理解为函数执行上下文。这个上下文有以下内容:
举个函数嵌套执行的栗子,a 函数当前在执行中,这个时候其中有条语句要执行 b 函数,这个时候可以简单理解为计算机做了以下的事情:
这个过程就是运行时函数调用栈,用栈这种数据结构来实现了上下文的切换。
我们上面说到过递归调用可以类比为普通嵌套调用,无非是这一次的执行上下文切换到了下一次的执行上下文,并且注意还有代码执行的控制(前面说过,递归是又回到了前面行数执行)。通过以上的描述我们可以得到一些思路,模拟递归,要解决两个主要问题:
执行上下文的切换,可以用栈来模拟。那么如何控制语句的执行顺序呢?下面来介绍一种 Continuation 技术。
来看个示例:
function test() {
console.log(1);
console.log(2);
console.log(3);
}
以上的输出是 123,我们现在要重新封装一个函数,内容一样,但是改变这三句代码的执行顺序,2 输出后继续回到 1,第二次输出 2 时再继续输出 3 并结束,也就是输出 12123。当然,不是要你重复写多余的 console.log 的。
计算机是通过 PC(Program Counter) 寄存器来取得下一条执行指令的地址。也就是说我们要找到一种模拟 PC 寄存器的工作的方案,看下面的代码:
function test2() {
let continuation = 0;
let count = 0;
while (true) {
switch (continuation) {
case 0:
console.log(1);
continuation = 1;
break;
case 1:
console.log(2);
count++;
continuation = count < 2 ? 0 : 2;
break;
case 2:
console.log(3);
return;
}
}
}
以上的方式就是用 continuation 变量来模拟 PC 寄存器的操作。根据逻辑来改变程序的执行路径,就可以模拟代码在我们想要的行数继续执行。下面就结合 Continuation 和模拟栈来康康怎么套路改写递归函数。
先来看看套路的模板代码:
function example(...args) {
const $stack = []; // 模拟栈操作,切换函数上下文。
let $result; // 记录每一次函数的执行结果。
/**
* 调用递归函数,压入新的函数上下文,入栈操作。
* 函数的参数、函数里面声明的变量都要放入上下文中。
*/
const $call = (...args) => {
$stack.push({
continuation: 0,
...args
});
};
/**
* 递归函数执行完毕,切换为上一次的函数上下文,出栈操作。
* 并记录函数的返回值。
*/
const $return = result => {
$stack.pop();
$result = result;
};
// 第一次调用。
$call(...args);
while ($stack.length > 0) {
const current = $stack[$stack.length - 1];
switch (current.continuation) {
// 将递归函数的代码拆分,利用 continuation 控制代码执行。
// ...
}
}
// 返回最后的结果。
return $result;
}
// 执行 example(...) 开始调用“递归”函数。
通过以上的代码可以看出,模板就是利用前面说到的两点来改写递归函数:
那么前面的阶乘递归函数用这个模板来改写后是这个样子的:
// n! = n * (n - 1) * (n - 2) * ... * 1
function getFactorial(n) {
if (n === 1) {
return 1;
} else {
return n * getFactorial(n - 1);
}
}
// 改为非递归。
function getFactorial2(n) {
const $stack = [];
let $result;
const $call = n => {
$stack.push({
continuation: 0,
n
});
};
const $return = result => {
$stack.pop();
$result = result;
};
$call(n);
while ($stack.length > 0) {
const current = $stack[$stack.length - 1];
switch (current.continuation) {
case 0: {
const { n } = current;
if (n === 1) {
$return(1);
} else {
$call(n - 1);
current.continuation = 1;
}
break;
}
case 1: {
const { n } = current;
$return(n * $result);
break;
}
}
}
return $result;
}
这里理解的重点在于如何拆分和改写代码,原则上调用递归函数的地方都要拆开。比如原代码是 return n * getFactorial(n - 1);,拆成了两个部分:
case 0: {
const { n } = current;
if (n === 1) {
$return(1);
} else {
$call(n - 1); // 调用递归函数的地方。
current.continuation = 1;
}
break;
}
case 1: {
const { n } = current;
$return(n * $result); // 这里是拿到子递归的返回值并返回这次递归的结果。
break;
}
因为要模拟函数调用的过程,等到有返回值后才能再执行后面的代码,所以必须要这样拆分才行。同时还要注意的是,因为是用 while 循环来改写递归,所以当涉及一些语法糖或者 for..in 这种循环的时候,必须要改写为对应的 while 循环代码才行。
下面再来看一个复杂点的例子,对象深拷贝递归函数:
// JS 对象深拷贝,简单实现。
function deepCopy(obj, map = new Map()) {
if (typeof obj === 'object') {
let res = Array.isArray(obj) ? [] : {};
if (map.has(obj)) {
return map.get(obj);
}
map.set(obj, res);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
res[key] = deepCopy(obj[key], map);
}
}
return map.get(obj);
} else {
return obj;
}
}
function deepCopy2(obj, map = new Map()) {
const $stack = [];
let $result = {};
const $call = (obj, map) => {
$stack.push({
continuation: 0,
obj,
map,
res: null,
keys: [],
key: ''
})
};
const $return = obj => {
$stack.pop();
$result = obj;
};
$call(obj, map);
while ($stack.length > 0) {
const current = $stack[$stack.length - 1];
switch (current.continuation) {
case 0:
{
const { obj, map } = current;
if (typeof obj === 'object') {
current.res = Array.isArray(obj) ? [] : {};
if (map.has(obj)) {
$return(map.get(obj));
break;
}
current.continuation = 1;
break;
} else {
$return(obj);
break;
}
}
case 1:
{
const { obj, map, res } = current;
map.set(obj, res);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
current.keys.push(key);
}
}
current.continuation = 2;
break;
}
case 2:
{
if (current.keys.length > 0) {
current.key = current.keys.shift();
const { obj, map, key } = current;
$call(obj[key], map);
current.continuation = 3;
break;
} else {
$return(map.get(current.obj));
break;
}
}
case 3:
{
current.res[current.key] = $result;
current.continuation = 2;
break;
}
}
}
return $result;
}
这个例子就出现了 for..in 这种循环,所以必须要先变成 while 循环可以处理的方式;还有一点要注意的是 res[key] = deepCopy(obj[key], map); 这句代码中调用了递归,那么 key 也在需要保存的上下文变量中。
理论上所有的递归函数都可以用以上的模板套路改写为非递归形式。但我觉得这种方式其实没有多大的实用性,本质上也是模拟了函数调用栈的实现,而且这种改写会使代码更新复杂和难以理解。但另一方面,我觉得理解和学习这种改写方式还是有价值的,可以加深函数调用栈和计算机怎么运行代码控制的理解,还是有点意思的。
欢迎 star 和关注我的 JS 博客:小声比比 JavaScript
在js中通过如果一个函数直接或间接调用函数本身,则该函数称为递归函数。
JavaScript生成树形菜单需求:首先这是一个数据集—js的类型,我们需要把生成一个tree形式的对象 : id,与pid之间的对应关系,当pid不存在,或pid:0的时候,这一项,应该为树的顶端,那么我们需要去重新建一次索引。
什么是递归组件?简单来说就是在组件中内使用组件本身,下面我们就来看看如何在项目中使用递归组件去解决我们上面问题。类似与信息分类的展示在我们的项目中是非常常见的形式,我们利用递归组件可以很好的去解决问题
最近遇到一个需求,平时被后台惯着直接返回了树形结构给到前端,前端对这种嵌套类型的数据(如地区的级联或菜单的树形结构)省掉了一层处理。换了个后台开发返回了扁平化的数组数据给到前端自己去处理如下data。突然有点慌......
循环数组或对象内每一项值,在 js 里原生已经提供了一个迭代器。凡是需要用到递归的函数参考迭代器模式,能写出更优雅,可读性更高的代码。
递归函数是在一个函数通过名字调用自身的情况下构成的,这种写法在函数有名字,而且名字以后也不会变的情况下是没有问题的。但是函数的执行与函数名factorial紧紧耦合在了一起
递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开
由于父元素的定位属性, 导致子元素及其孙元素等的offsetLeft和offsetTop变得和预期不一致(预期上都是到屏幕左边和上边的位置), 由于需要做鼠标拖动旋转和鼠标框选
递归含义:在某时某刻某个条件下调用包含自己的函数;注意点:⑴递归过程中一定要加限制条件,要不然会陷入死循环:递归有个过程,不是一步到位的,这一点尤其重要
项目中有一个需求,一个tabBar下面如果没有内容就不让该tabBar显示,当然至于有没有内容,需要我们通过请求的来判断,但是由于请求是异步的,如何让请求按照tabBar的顺序进行?
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!