递归改循环方案,用循环遍历未知深度的树形结构
在很多时候,我们在遍历的时候会选择用递归来实现,比如深拷贝两个对象,拍平数组啥的,大多数情况下,我们的栈的深度不会太深,使用递归并没用什么问题。
下面,我们来讨论下特殊场景下,如何用循环来代替递归。
递归的一些优缺点
缺点: 1.递归在不同的浏览器,栈溢出的极限值不一样,谷歌78达到3万多,ie11还不到1万
缺点: 2.递归要保存函数的很多信息,理论上来说,空间上是要占用更多的储存空间,执行效率上是会略慢一些(没有实测)
优点: 递归更加通俗易懂(相比较改循环而言)
查看当前浏览器调用栈的长度可以用以下代码
(()=>{
let maxStackNumber = 0;
function test() {
return test(maxStackNumber++)
}
try {
test()
} catch (err) {
console.log(maxStackNumber)
}
})()
如何用递归遍历未知深度的树形结构
我们以实现一个“简单”的深拷贝代码为例(实际业务中会考虑很多类型,本文的重点不在深拷贝如何实现上,在这里我们只考虑最简单的)
(function () {
function isObj(o) {
return Object.prototype.toString.call(o)==="[object Object]"
}
function isArray(o) {
return Object.prototype.toString.call(o)==="[object Array]"
}
function assign(target,obj) {
for(let key in obj){
if(Object.hasOwnProperty.call(obj,key)){
if(isObj(obj[key])){
target[key]=target[key]||{};
assign(target[key],obj[key])
}else if(isArray(obj[key])){
target[key]=target[key]||[];
assign(target[key],obj[key])
}else {
target[key]=obj[key]
}
}
}
return target
}
const obj1={a:1,b:{c:2}};
const obj2={a:2,b:{d:3}};
return assign(obj1,obj2);
})();
如何将上述递归改造成循环?
递归改循环方案
栈中信息模拟
栈中保存函数的信息,如参数,返回值,作用域,函数体
根据具体的使用场景来选择保存哪些信息,这里以深拷贝为例,我们保存以下函数信息
- 参数
- 函数体中遍历的键集合(因为我们的for循环可能会在中间的时候暂停,进入下一级循环,所以在这里保存遍历的键集合)
- 函数体中遍历的键的索引(配合上面的键集合来使用)
class FnData{
setArgs(...args){//存储参数集合
this.args=args;
}
setKeys(keys){//存储当前对象的键集合
this.keys=keys
}
setIndex(index){//存储遍历位置
this.index=index
}
}
进栈出栈模拟
我们使用递归会自动进栈,自动出栈,所以我们需要模拟同样的行为,但for循环是从上至下执行,而递归是从外往内执行,这其实是有本质上的区别,
但我们可以用跳转来实现同样的效果,类似与goto语句,但js没有goto语句,幸运的是这不重要,我们可以用标签来模拟。
function assign(target,obj) {
//新建是一个栈集合
const stack = new Array(0);
//存储上述递归函数的相关信息
let fnData = new FnData();
fnData.setIndex(0);
fnData.setArgs(target,obj);
fnData.setKeys(Object.keys(obj));
//将信息推入栈
stack.push(fnData);
//while加标签,模拟goto,只要栈内有信息,则一直执行
outside:
while (stack.length>0){
//取出栈顶元素
let curFnData = stack[stack.length-1];
let [_target,_obj]=curFnData.args;
let keys = curFnData.keys;
//取出栈中遍历的索引,从上次没执行完的地方继续执行
for(let i=curFnData.index;i<keys.length;i++){
let key = keys[i];
//设置当前键已经遍历过,索引后移
curFnData.setIndex(i+1);
if(Object.prototype.hasOwnProperty.call(_obj,key)){
if(isObj(_obj[key])||isArray(_obj[key])){
_target[key]=_target[key]||(isArray(_obj[key])?[]:{});
//如果是对象,表明我们要进入下一层了,保存所需要的信息
let _fnData = new FnData();
_fnData.setIndex(0);
_fnData.setArgs(_target[key],_obj[key]);
_fnData.setKeys(Object.keys(_obj[key]));
//入栈
stack.push(_fnData);
/*
* 核心是这个
* 我们将下一层的信息推入栈中后,会中断当前的遍历,强制跳转至外层while循环,此时循环体会重新执行,
* 而栈顶的信息已经改变,变成了子级,等到子级遍历完毕出栈后,才会继续当前的遍历,
* 此时因为我们之前已经保存了索引信息,所以我们可以直接从索引位置开始
* */
continue outside
}else {
_target[key]=_obj[key]
}
}
}
//出栈,相当于从子级往外跳一层
stack.pop()
}
return target
}
完整代码
(function () {
function isObj(o) {
return Object.prototype.toString.call(o)==="[object Object]"
}
function isArray(o) {
return Object.prototype.toString.call(o)==="[object Array]"
}
class FnData{
setArgs(...args){//存储参数集合
this.args=args;
}
setKeys(keys){//存储当前对象的键集合
this.keys=keys
}
setIndex(index){//存储遍历位置
this.index=index
}
}
function assign(target,obj) {
//新建是一个栈集合
const stack = new Array(0);
//存储上述递归函数的相关信息
let fnData = new FnData();
fnData.setIndex(0);
fnData.setArgs(target,obj);
fnData.setKeys(Object.keys(obj));
//将信息推入栈
stack.push(fnData);
//while加标签,模拟goto,只要栈内有信息,则一直执行
outside:
while (stack.length>0){
//取出栈顶元素
let curFnData = stack[stack.length-1];
let [_target,_obj]=curFnData.args;
let keys = curFnData.keys;
//取出栈中遍历的索引,从上次没执行完的地方继续执行
for(let i=curFnData.index;i<keys.length;i++){
let key = keys[i];
//设置当前键已经遍历过,索引后移
curFnData.setIndex(i+1);
if(Object.prototype.hasOwnProperty.call(_obj,key)){
if(isObj(_obj[key])||isArray(_obj[key])){
_target[key]=_target[key]||(isArray(_obj[key])?[]:{});
//如果是对象,表明我们要进入下一层了,保存所需要的信息
let _fnData = new FnData();
_fnData.setIndex(0);
_fnData.setArgs(_target[key],_obj[key]);
_fnData.setKeys(Object.keys(_obj[key]));
//入栈
stack.push(_fnData);
/*
* 核心是这个
* 我们将下一层的信息推入栈中后,会中断当前的遍历,强制跳转至外层while循环,此时循环体会重新执行,
* 而栈顶的信息已经改变,变成了子级,等到子级遍历完毕出栈后,才会继续当前的遍历,
* 此时因为我们之前已经保存了索引信息,所以我们可以直接从索引位置开始
* */
continue outside
}else {
_target[key]=_obj[key]
}
}
}
//出栈,相当于从子级往外跳一层
stack.pop()
}
return target
}
const obj1={a:1,b:{c:2}};
const obj2={a:2,b:{d:3}};
return assign(obj1,obj2);
})();
性能
今天太晚了,改天测试下
总结
一般情况下,我们还是使用递归为佳,因为改循环的方案,就像使用goto一样难受,很对语言不支持goto是有原因的,太难受,跳来跳去很绕,
但如果我们有层级很深或者追求性能的时候,可以尝试将递归改为循环,你就可以跳出系统的限制,自己决定栈的深度。
结束语:如果遇到了问题,欢迎在评论区交流,如果觉得不错,可以点赞和收藏,持续更新。
博客中标注原创的文章,版权归原作者 苦中作乐才是人生巅峰所有;转载或者引用本文内容请注明来源及原作者;