一天一个小知识(1):js递归函数

559 阅读6分钟

递归函数介绍

递归函数,什么是递归函数,通俗地讲,递归函数就是不断的重复调用自己,形成一个循环的过程,这个过程就是递归,函数就是递归函数。而使用递归函数的主要目的,当然是为了解决我们实际开发中遇到的问题,下面看个简单的递归函数,求和, 假定我们有一个数组需要进行求和计算:

`

var ar = [1, 3, 5, 7, 9];
function ordinaryConut (ar) {	// 普通求和方式
		let n = 0;
		for (let i = 0; i < ar.length; i++) {
			n += ar[i]
		}
		return n
	}
	let result1 = ordinaryConut(ar);
	console.log(result1, '普通求和'); // 输出25
	
	
function recursionCount (ar) {  // 递归求和方式
		let i = 0;
		function ordinary (n) {  // 这个是递归函数
			if (n >= ar.length) {   // 递归结束条件
				return;
			}
			i += ar[n]; // 执行逻辑
			ordinary(n + 1) // 自调用执行递归
		}
		ordinary(0);    // 执行递归函数
		return i;   // 返回递归计算出来的结果
	}
	let result2 = recursionCount (ar);
	console.log(result2, '递归求和');  // 输出25

`

上面这个例子,我给递归函数写了足够多的注释,目的就是为了让大家认识递归,什么是递归,看了上面的例子大家是不是觉得递归反而代码,变得更多了,还不益于阅读,还影响性能,这是个小例子,自然是有点大材小用的感觉,下面我们一步一步来拆分这个递归函数:

    1. 这个递归函数存在于另外一个函数体里面,这样写是有他的原因的,一.当然是为了让他益于理解;二.这是因为在我们实际开发过程当中绝大多数都是需要它在一个特定的代码块里去重复进行逻辑处理,而它处理的结果值存在一个私有的作用域里的变量上,而这个变量不会受外界干扰和污染的,而本例的私有变量自然就是i了。
    1. 递归的结束条件,本例的结束条件是当n>=ar.length时,就return出去,中止循环,此处划重点,递归的结束条件是重点的中的重点,因为如果你没有考虑好结束条件,那么这个递归函数就会陷入死循环
    1. 执行逻辑,本例的执行逻辑i+=ar[n]这么一句话,虽然本例是一句话,但是它这里却是递归真正处理逻辑核心代码块,没有这个部分,你的递归没有任何意义。而理解到这一点,你的代码就完全可以基于你自己的业务需求去写业务逻辑了;
    1. 自执行,所谓自执行,不用说,那就是函数体内自己调用自己,正是因为这个机制,递归函数,就同样可以不依赖for,while等循环语句来实现循环的过程;

递归函数的优劣

递归的优点:

  • 递归在处理一些逻辑相对比较复杂的业务逻辑的时候,递归函数的代码体,会更加简洁,结构清晰,更易于阅读;

递归的缺点:

  • 递归会更加多的占用系统资源,进而影响性能,为什么这么说呢,因为我们知道,在js当中,一个函数执行,系统会给这个函数分配一个内存空间,我们称之为“栈”,而我们的递归函数就是在不断的重复执行,那么系统就会给他分配每次执行的内存空间,而每个进程的栈的容量是有限的,当调用的层次太多时,就会超出栈的容量,从而导致栈溢出,这就导致了性能问题;

个人意见:

我个人对递归的使用观念是,该用就咋用,不用考虑那么多。为什么这么说呢,我在网上也看过别人写的博客,多数人都是说,递归对性能的影响很大,建议慎用或者少用什么的,而我要说是,现在是2019年,我们的计算机早就处于一个性能过剩的时代了,现在计算机的配置,都是高的不得了,也就递归对性能的影响,可以忽略N个层级了。像我们一般递归的便利,几乎是影响不到的。

递归实战

递归的应用,最典型的场景,莫过于就是对树列表的数据处理,那么本小节将模拟一个树的数据展示,同样将使用普通的处理方式和递归处理方式给大家做比较:

  • 要处理的数据

`

let data = [
			{id: 1, value: '动物'}, {id: 2, value: '植物'}, {id: 3, value: '微生物'}, {id: 4, value: '无机物'},
			{id: 5, parentId: 1, value: '脊椎类动物'}, {id: 6, parentId: 2, value: '木科植物'}, {id: 7, parentId: 1, value: '无脊椎类动物'}, {id: 8, parentId: 2, value: '草本植物'},
			{id: 9, parentId: 3, value: '细菌'}, {id: 10, parentId: 5, value: '老虎'}, {id: 11, parentId: 5, value: '狮子'}, {id: 12, parentId: 5, value: '猎豹'},
			{id: 13, parentId: 6, value: '松树'}, {id: 14, parentId: 6, value: '樟树'}, {id: 15, parentId: 6, value: '桦树'}, {id: 16, parentId: 7, value: '虾'},
			{id: 17, parentId: 7, value: '章鱼'}, {id: 18, parentId: 3, value: '真菌'}, {id: 19, parentId: 3, value: '病毒'}, {id: 20, parentId: 4, value: '黄金'},
			{id: 21, parentId: 8, value: '牡丹花'}, {id: 22, parentId: 8, value: '四叶草'}, {id: 23, parentId: 4, value: '白银'}, {id: 24, parentId: 4, value: '玉石'},
			{id: 25, parentId: 4, value: '珍珠'}, {id: 26, parentId: 9, value: '球状杆菌'}
		]

`

  • 普通处理方式

`

function ordinaryFn (data) {
		var firstData = [], otherData = [];
		for (let i = 0; i < data.length; i++) {
			data[i].children = [];
			if (!data[i].parentId) {
				firstData.push(data[i])
			} else {
				otherData.push(data[i])
			}
		}		
		for (let i = 0; i < firstData.length; i++) {
			var secendOthers = []
			for (let j = 0; j < otherData.length; j++) {
				if (firstData[i].id === otherData[j].parentId) {
					firstData[i].children.push(otherData[j])
				} else {
					secendOthers.push(otherData[j])
				}
			}
			for (let l = 0; l < firstData[i].children.length; l++) {
				var cur = firstData[i].children[l], threeData = [];
				for (let n = 0; n < secendOthers.length; n++) {					
					if (cur.id === secendOthers[n].parentId) {
						cur.children.push(secendOthers[n])
					} else {
						threeData.push(secendOthers[n])
					}
				}					
			}
		}
		return firstData;
	}
	let result1 = ordinaryFn (data);
	console.log(result1, '普通方式')

`

  • 递归处理方式

`

function classify (arr) {	// 递归方式1
		let first = [], others = [];
		arr.forEach(item => {
				item.children = [];
				if (!item.parentId) {
					first.push(item)
				} else {
					others.push(item)
				}
			});
		function recursion (arr, ary) {
			arr.forEach(item => {
				let other = []
				ary.map(cur => {
					if (item.id === cur.parentId) {
						item.children.push(cur)
					} else {
						other.push(cur)
					}
				})
			recursion(item.children, other);
		})
	}
	recursion(first, others);
	return first;
}			
	let result2 = classify(data);
	console.log(result2, '递归方式');
            
        function treeFn (data) { // 采用非递归方式处理树形数据,使用数组方法reduce
            const treeData = data.reduce((prev, next) => {
			prev[next.id] = next
			return prev
	}, {})	// 核心步骤,指定reduce第二个参数指定初始值,也就是设定prev这个参数的初始值
	console.log(treeData)
	const result = data.reduce((prev, next) => {
	let parentId = next.parentId;
	let parentNode = treeData[parentId];
	if (parentNode) {
parentNode.children ? parentNode.children.push(next) : parentNode.children = [next];
	} else if (!parentId) {
		prev.push(next)
	}
	return prev
        }, []) // 指定第二个参数为数组
	return result
}

`

  • 期望的数据结果形式

通过上面的代码比较,相信大家都可以清晰的看出来,孰优孰劣,普通的处理方式,虽然也处理出来了,但是大家想一想,如果你这个数据的层级在多加一级呢,是不是就处理不了?而且可扩展性极低,阅读性也不易于理解。但是递归的方式就不会存在这种问题,不管你多少层级,都可以轻松应对,可扩展性好,易于阅读。

小结

函数递归调用是我们处理一些数据的常见手段,该方式也属于高阶函数的应用技巧,熟练的掌握与应用,将会对我们处理复杂的业务数据时,将会更加得心应手。喜欢的朋友点个关注,thanks。