函数的尾调用及优化,尾递归及优化

尾调用

一个函数最后一步是调用另一个函数,即尾调用

	function foo() {
		return bar()
	}

以下三种情况不属于尾调用

	function bar (x) {
		let y = g(x);
		return y
	}

	function bar (x) {
		return g(x) + 1;
	}

	function bar (x) {
		g(x)
	}

第一种是调用完还有赋值操作,
第二种也是调用完还在操作,即使是同一行
第三种等于下面这样的写法

	function bar (x) {
		g(x);
		return undefined;
	}

写的真奔放啊,return都不写还想返回别的函数?

	function foo () {
		if (!false) {
			return g(x);
		}
		
		return g(x);
	}

上面的都属与尾调用,尾调用不需要必须写在函数末尾,但是必须是最后一步,并且不可以再操作它。

尾调用优化

尾调用之所以与其他调用不同,在于它的特殊的调用位置

函数调用会形成一个(调用记录),又称(调用帧)
保存调用位置和内部变量等。如果在函数A中调用了一个B。
那么A的调用帧上方,就会有一个B的调用帧。
等到B运行结束返回给A,B的调用帧才会消失
如果B里面还有一个函数C,那么B的调用帧还会有一个C的调用帧,以此类推。
所有的调用帧,就会形成一个调用栈

尾调用的机制是,它就是函数的最后一步,所以不需要依赖于外层函数的调用帧,因为调用位置,和内层的变量等信息不会再使用到。

只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了

	function foo () {
		let m = 1;
		let n = 2;
		return bar(m + n);
	}
	foo();

	// 等同于
	function foo () {
		return bar(3)
	}
	foo();

	bar(3);

上面代码,如果bar函数不是尾调用,那么它就依赖于调用位置,内部变量m 和 n等。

但由于使用了尾调用机制,不需要保留foo函数的调用帧,调用到bar的时候,foo函数就已经结束了。

所以执行到最后一步完全可以删除foo函数的调用帧,只保留bar的调用帧

这就是尾调用优化,只保留内层函数的调用帧,如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大减少内存。

注意:只有在不适用外层函数的内存变量,内层函数的调用帧才会取代外层函数的调用帧,否则尾调用失效

	function bar () {
		let car = 'BMW'
		function foo (wife) {
			return wife + car
		}
		return foo(male)
	}

上面代码中,函数foo用到了外层函数bar的变量,所以尾调用优化失效。

其实尾调用优化就是不让你使用闭包机制,大家都是知道闭包会造成内存泄漏。

所以最好使用let声明变量,否则还会污染全局变量

各大浏览器正在慢慢支持,请慎用!

尾递归

函数调用自身称为递归,如果尾调用自身,称为尾递归

首先递归本就非常消耗内存,一时间要保存n个调用帧,很容易发生(栈溢出),使用尾递归,由于只存在一个调用帧,所以永远不会造成栈溢出

使用递归写一个n的阶乘

	function factorial (n) {
		if (n === 1) return 1;
		return n * factorial(n - 1);
	}

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存
n个调用帧,复杂程度 fn (n)

使用尾递归写一个n的阶乘

	function factorial (n, total) {
		if (n === 1) return total;
		return factorial (n - 1, n * total)
	}

	factorial(5, 1)

尾递归就是把函数的输入再输出,达到函数式编程特性

并且会把阶乘的结果保存着,当等于的时候把这个乘完的值直接return出去,要比普通递归那种复杂度小太多 fn (5)

递归函数最常用的就是 Fibonacci 数列
1 2 3 5 8 13 21 34 55 89

前两位的和等于第三位

使用递归实现 Fibonacci 数列

	function fibonacci (n) {
		if (n <= 1) return 1;
		return fibonacci(n - 1) + fibonacci(n - 2);
	}

看似两条语句,但是内部特别复杂,感兴趣可以打印一下return后面的函数结果,

原因就是递归本就浪费性能,再加上fibonacci数列依靠前两位计算第三位的特性,使得程序更加的复杂。

使用尾递归实现 Fibonacci 数列

	function finbonacci (n, ac1 = 1, ac2 = 1) {
		if (n <= 1) return ac2;
		
		return finbonacci (n - 1, ac2, ac1 + ac2);
	}

下面是函数调用的结果,其实琢磨一下,比比划划就明白了。
100 1 1
99 1 2
98 2 3
97 3 5

和之前的n的结成一样,ac2保存着当前位的结果。

由此可见,尾调用优化对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格,es6也是如此,所有ECMAscript的实现,都必须部署,尾调用优化。这就是说,ES6中只要使用尾递归,就不会发生栈溢出

所以尾调用优化,很重要。

递归函数的改写

尾递归的实现往往需要改写函数内容,确保最后一步只调用自身,那么就需要把用到内部变量改写成函数的参数。

比如上面的n的阶乘,需要一个total变量,明明只需要一个n告诉我阶乘是几就好,写了两个参数显得不那么语义化,让第一眼看上去就很不舒服

所以可以在尾递归外,再提供一个函数

	// n的变量写成了参数,这样尾调用就不会使用外部调用帧了
	function jc (n, total) {
		if (n === 1) return total;
		return jc(n - 1, n * total);
	}

	function Jc (n) {
		return jc(n, 1)
	}
	Jc(10);

这样Jc开启n的阶乘,返回了jc方法来实现n的阶乘的结果

总结:只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

严格模式下才能使用尾调用
原因很简单,严格模式下会取消两个函数变量

func.arguments – 调用当前函数参数
func.caller – 调用当前函数的函数

尾调用优化时,函数的调用栈会改写,因此这两个变量会失真,所以严格模式下禁用了它们

	function test () {
		'use strict';
		test.arguments;
		// error
		test.caller
		// error
	} 
	test();

尾递归优化

尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。

它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

	function sum(x, y) {
	  if (y > 0) {
	    return sum(x + 1, y - 1);
	  } else {
	    return x;
	  }
	}
	
	sum(1, 100000)

上面的函数会报错,因为超出了调用栈的最大次数

下面来进行尾递归的优化

	function tco(f) {
	  // 递归完的累加值
	  var value;
	  // 是否开启递归
	  var active = false;
	  // 储存当前的递归累加值,剩余递归次数
	  var accumulated = [];
		
	  // 进入尾递归优化
	  return function accumulator() {
	    // 把参数列表放进去:递归累加值,剩余次数,symbol。。。
	    accumulated.push(arguments);
	    // 开启递归
	    if (!active) {
	      active = true;
	      // 根据数组长度来进行循环 长度为1,(arguments)
	      while (accumulated.length) {
	        // 返回arguments第一个参数的值
	        value = f.apply(this, accumulated.shift());
	      }
	      // 关闭递归
	      active = false;
	      // 返回当前递归之后累加的值
	      return value;
	    }
	  };
	}

	var sum = tco(function(x, y) {
	  // 第二个参数大于零就一直递归
	  if (y > 0) {
	    return sum(x + 1, y - 1)
	  }
	  // 小于零时,停止递归
	  else {
	    return x
	  }
	});

	sum(1, 100000)
	// 100001

上面这种形式可谓是牛牛牛,

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active

默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。

然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;

accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。

这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层

内容借鉴了阮一峰大神,也有部分个人理解

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试
应支付0元
点击重新获取
扫码支付

支付成功即可阅读