JavaScript 尾递归优化

栏目: 编程工具 · 发布时间: 5年前

内容简介:尾调用(Tail Call)是函数式编程的一个概念,意思是在某个函数的最后一步调用另一个函数。在函数调用时,会在内存形成调用帧,用来保存调用位置和内部变量等信息,而嵌套函数则会形成调用栈。尾调用由于是函数的最后一步操作,因此不需要保留外层函数的调用帧,利用函数的这个特性进行优化,就是尾调用优化(Tail Call Optimization)。利用尾调用优化来优化递归函数,便是尾递归优化。因为递归函数在到达递归边界之前会不停保存调用帧,一旦递归深度过大,就容易发生栈溢出的错误,因此需要使用尾递归优化来解决这个

尾调用优化

尾调用(Tail Call)是函数式编程的一个概念,意思是在某个函数的最后一步调用另一个函数。在函数调用时,会在内存形成调用帧,用来保存调用位置和内部变量等信息,而嵌套函数则会形成调用栈。尾调用由于是函数的最后一步操作,因此不需要保留外层函数的调用帧,利用函数的这个特性进行优化,就是尾调用优化(Tail Call Optimization)。

尾递归优化

利用尾调用优化来优化递归函数,便是尾递归优化。因为递归函数在到达递归边界之前会不停保存调用帧,一旦递归深度过大,就容易发生栈溢出的错误,因此需要使用尾递归优化来解决这个问题。

递归函数改写成尾递归

以斐波那契数列求解的递归函数为例:

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

console.log(fibonacci(100)) // ......

此时当 n 大于 50 时运算速度极慢,大于 100 时基本无响应。使用柯里化改写该递归函数,将多参数转换为单参数形式,即可得到尾递归优化的求解函数:

function currying(f, n1, n2) {
  return function(m) {
    return f.call(this, m, n1, n2);
  };
}

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

var fib = currying(fibonacci, 1, 1);
fib(10000) // Infinity

在 ES6 中可以使用默认参数,因此有了最简单的实现方法:

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

fibonacci(10000) // Infinity

尾递归优化的实现方法

由于某些环境下无法直接使用尾调用优化,因此需要自己实现。实现原理也不复杂,就是用循环语句代替递归语句。

实现1:蹦床函数

还是以斐波那契数列为例

// 递归函数
function fibonacci(n, n1 = 1, n2 = 1) {
  if (n <= 2) return n2;
  else return fibonacci.bind(null, n - 1, n2, n1 + n2); // 返回一个新的函数
}

// 蹦床函数
function trampoline(f) {
  while (f && f instanceof Function) f = f();
  return f;
}

trampoline(fibonacci(10000)); // Infinity

首先将递归函数进行尾调用优化,然后在函数返回部分利用 bind() 方法返回一个全新的函数,以便蹦床函数调用。蹦床函数的核心部分是一个循环,只要传入的参数是一个函数,就会调用这个函数,直到传参不再是一个函数为止。结合起来看可以发现,蹦床函数的作用就是不断用一个新的函数替代原函数,直到遇到递归边界,返回结果。需要注意的是,蹦床函数并不是真正的尾递归优化,因为蹦床函数每次循环会产生一个新的函数帧。下面的实现才是真正的尾递归优化。

实现2:尾递归优化函数

// 尾递归优化函数
function tailCallOptimize(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var fibonacci = tailCallOptimize(function(n, n1 = 1, n2 = 1) {
  if (n <= 2) return n2;
  else return fibonacci(n - 1, n2, n1 + n2);
});

console.log(fibonacci(10000)); // Infinity

这个实现稍微有点复杂,要理解 tailCallOptimize() 这个函数,其关键点就在中的“active”和“accumulated”,一个用来保存函数当前状态,一个用来保存当前递归的参数。首次运行时,保存参数,“active”变为“true”,意思是此时已经进入尾递归优化阶段,之后的每次调用,都不会进入 if 语句中,仅仅保存该次调用的参数,直到最后到达递归边界,返回一个值,此时“accumulated”为空,跳出循环,恢复状态,返回结果。整个过程的核心部分只有一个函数调用帧,因此这才是真正的尾递归优化。

测试

什么时候可以直接使用尾递归优化,什么时候要自己实现,有必要做一个测试看看,测试使用的代码如下:

"ues strict"

function fibonacci(n, n1 = 1, n2 = 1) {
  if (n <= 2) {
    console.trace(); // 跟踪函数调用帧
    return n2;
  }
  else return fibonacci(n - 1, n2, n1 + n2);
}
  1. Chrome 72.0.3626.121
fibonacci(5)

// 以下为输出
fibonacci @ VM92:3
fibonacci @ VM92:6
fibonacci @ VM92:6
fibonacci @ VM92:6
(anonymous) @ VM131:1

5
  1. Firefox Firefox 65.0.2
fibonacci(5)

// 以下为输出
fibonacci debugger eval code:3 
fibonacci debugger eval code:6 
fibonacci debugger eval code:6 
fibonacci debugger eval code:6 
<anonymous> debugger eval code:1 

5
  1. Node 11.0.0
fibonacci(5)

// 以下为输出
Trace
    at fibonacci (repl:3:13)
    at fibonacci (repl:6:15)
    at fibonacci (repl:6:15)
    at fibonacci (repl:6:15)
    at repl:1:1
    at Script.runInThisContext (vm.js:119:20)
    at REPLServer.defaultEval (repl.js:331:29)
    at bound (domain.js:395:14)
    at REPLServer.runBound [as eval] (domain.js:408:12)
    at REPLServer.onLine (repl.js:644:10)
5

查了相关资料发现如下说法:“尾归调用的想法是好的,但是落地的时候出现了分歧,node在后续的版本中支持过尾归调用,但后续给去掉了。浏览器上只有safari支持,而其他浏览器上并不支持。”由此可知目前最主流的引擎都没有实现尾递归优化,如果真的需要使用,还是需要自己使用循环的方式来实现。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

金字塔原理

金字塔原理

[美] 巴巴拉·明托 / 王德忠、张珣 / 民主与建设出版社 / 2002-12 / 39.80元

《金字塔原理》是一本讲解写作逻辑与思维逻辑的读物,全书分为四个部分。 第一篇主要对金字塔原理的概念进行了解释,介绍了如何利用这一原理构建基本的金字塔结构。目的是使读者理解和运用简单文书的写作技巧。 第二篇介绍了如何深入细致地把握思维的环节,以保证使用的语句能够真实地反映希望表达的思想要点。书中列举了许多实例,突出了强迫自己进行“冷静思维”对明确表达思想的重要性。 第三篇主要针对的......一起来看看 《金字塔原理》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器