• 什么是函数尾调用和尾递归
  • 函数尾调用与尾递归的应用

一、什么是函数的尾调用和尾递归

函数尾调用就是指函数的最后一步是调用另一个函数。

 //函数尾调用示例一
function foo(x){
return g(x);
}
//函数尾调用示例二
function fun(x){
if(x > 0){
return g(x);
}
return y(x);
}

调用最后一步和最后一行代码的区别,最后一步的代码并不一定会在最后一行,比如示例二。还有下面这一种不能叫做函数尾调用:

 // 下面这种情况不叫做函数尾调用
function fu(x){
var y = 10 * x;
g(y);
}

为什么这种情况不叫作函数的尾调用呢?原因很简单,因为函数执行的最后一步是return指令,这个指令有两个功能,第一个功能是结束当前函数执行,第二个功能是将指令后面的数据传递出去(函数返回值)。而这个return指令不管有没有声明都会执行,没有声明的情况下返回的数据是undefined,所以上面的代码实际上是以下结构:

 function fu(x){
var y = 10 * x;
g(y);
return undefined;
}

return指令是先关闭函数,然后再返回数据。说到这里,就会引发一个问题出来,如果最后一步不是函数尾调用会怎么样?return指令后面是下面这种情况,会发生什么?

 //数的阶乘
function factorial(n){
if(n === 1 || n ===0 ) return 1;
return n * factorial(n - 1);
}

上面这个数的阶乘算法示例不能叫做函数尾调用,因为最后一步是乘积计算,不是纯粹的函数调用。

二、函数尾调用与尾递归的应用

尾调用本质上就是说函数最后执行的一步return指令中,返回数据的这一部分是一个函数执行。看似这个简单的指令和其简单明了的功能,并没有特别之处。但是函数执行时,会在内存形成一个“调用记录”,通常被称为“调用帧”。注意,是在函数执行时内部调用,也就是说是在return指令触发之前的函数调用,因为return指令之后的函数调用会产生一个独立的函数调用栈,而不是在原来的函数调用栈上添加调用帧。

我们直到浏览器分配的内存空间是有限的资源,也就是说函数的调用栈内存是有限的,如果函数出现很大的循环嵌套调用函数,每个嵌套的函数调用都会在原来的函数调用栈顶上添加一个调用帧,像上面的数的阶乘如果传入的参数是100的话,就会在factorial函数调用栈上产生99个调用帧,如果实参再大一点呢?1000或者更多,这种无限堆叠的可能肯定会带来一个风险,就是栈溢出。

再来看下面这个示例:

 function fb(n){
if(n == 1 || n == 2){
return 1
}
return fb(n - 1) + fb(n - 2);
}
console.log(fb(100)); //堆栈溢出,浏览器崩溃

上面这个示例(斐波那契数列)有跟乘介算法一样的问题,就是都是在return指令后面对函数执行结果在计算,而这种计算实际上发生当前函数上,而且还会在函数的调用栈上不断增加调用帧,直到符合程序出口逻辑才会停止。但是当计算的数值达到一定程度时就会导致堆栈溢出,造成浏览器奔溃。

说了这么多,一直没有明确解析什么是尾递归,其实没什么可以解析的,就是在return指令后面调用自身函数执行。然后下面就是使用尾递归和ES的默认参数解决阶乘和斐波那契数列算法的调用帧溢出问题:

 //使用ES6的默认值 + 尾递归实现阶乘算法
function factorial1(n,total=1){
if(n === 1 || n === 0 ) return total;
n += 1;
return factorial(n - 1, n * total);
}
//使用ES6的默认值 + 尾递归实现斐波那契数列数列算法
function fb1(n, ac1 = 1, ac2 = 1){
if( n === 1 || n === 2) return ac2;
return fb1 (n - 1, ac2, ac1 + ac2);
}

在阮一峰老师的《ES6标准入门第三版》P127,中发现老师的两个算法在计算上值都少计算一位,比如老师的阶乘计算5的阶乘结果是24,这个结果一开始令我疑惑不解,个人推断老师的思路是按照计算机的计数方式(从0开始),其参数指定的是阶乘结果的索引,采用参数指定计算值所在结果集合的索引。不知道这个推测是否正确,如果有不对的地方还请各位指正。

而我在示例中采用的是数值的阶乘结果,不是阶乘结果表中的索引。

JavaScript函数尾调用与尾递归的更多相关文章

  1. javascript专题系列--尾调用和尾递归

    最近在看<冴羽的博客>,讲真,确实受益匪浅,已经看了javascript 深入系列和专题系列的大部分文章,可是现在才想起来做笔记.所以虽然很多以前面试被问得一脸懵逼的问题都被“一语惊醒梦中 ...

  2. JavaScript文件中调用AngularJS内部方法或改变$scope变量

    需要在其他JavaScript文件中调用AngularJS内部方法或改变$scope变量,同时还要保持双向数据绑定: 首先获取AngularJS application: 方法一:通过controll ...

  3. 跨JavaScript对象作用域调用setInterval方法

    跨JavaScript对象作用域调用setInterval方法: var id = window.setInterval(function() {foofunc.call(this);}, 200);

  4. [转]C# winform与Javascript的相互调用

    C# winform与Javascript的相互调用 <html> <head> <meta http-equiv="Content-Language" ...

  5. ASP.Net:Javascript 通过PageMethods 调用后端WebMethod方法 + 多线程数据处理 示例

    ASP.Net:Javascript 通过PageMethods 调用后端WebMethod方法 + 多线程数据处理 示例 2012年04月27日 16:59:16 奋斗的小壁虎 阅读数:4500   ...

  6. web端高德地图javascript API的调用

    [转载https://www.cnblogs.com/zimuzimu/p/6202244.html]web端高德地图javascript API的调用 关于第三放地图的使用,腾讯.百度.高德 具体怎 ...

  7. Unity 3d C#和Javascript脚本互相调用 解决方案(非原创、整理资料,并经过实践得来)

    Unity 3d C#和Javascript脚本互相调用 解决方案 1.背景知识 脚本的编译过程分四步: 1. 编译所有 ”Standard Assets”, “Pro Standard Assets ...

  8. Javascript&Html-延迟调用和间歇调用

    Javascript&Html-延迟调用和间歇调用 Javascript 是一种单线程语言,所有的javascript任务都会放到一个任务列表中,这些javascript任务会按照插入到列表中 ...

  9. [Swift通天遁地]一、超级工具-(6)通过JavaScript(脚本)代码调用设备的源生程序

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

随机推荐

  1. linux如何离线加载docker镜像?

    1. 在已经部署了镜像的机器上获取镜像 1.1 获取镜像名 docker images 1.2 打包选中对应的镜像 docker save <image_name> -o <imag ...

  2. ObjectAnimator属性动画示例代码

    import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.app.Ac ...

  3. HTTP状态码分类及异常状态码处理

    1xx:表示临时响应100:(继续)请求者应当继续提出请求.服务器返回此代码表示已收到请求的第一部分,正在等待其余部分101:(切换协议)请求者已要求服务器切换协议,服务器已确认并准备切换 2xx:表 ...

  4. RabbitMQ学习之:(十一)AMQP.0-10规范,中文翻译1,2,3章 (转载)

    From:http://blog.sina.com.cn/s/blog_4aba0c8b0100p6ho.html From: http://blog.sina.com.cn/s/blog_4aba0 ...

  5. java数据机构之自定义栈

    一.栈的特点 1.线性数据结构 2.后进先出 二.使用数组来实现栈 //使用数组来实现栈 public class MyArrayStack<E> { //保存数据 private Obj ...

  6. 【操作系统】【C/C++开发】内存管理

    内存管理 操作系统对内存的划分和动态分配,就是内存管理的概念.有效的内存管理在多道程序设计中非常重要,不仅方便用户使用存储器.提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器.内存管理的功能有: ...

  7. Go语言中数组的内部实现和基础功能

    数组的内部实现和基础功能 因为数组是切片和映射的基础数据结构.理解了数组的工作原理,有助于理解切片和映射提供的优雅和强大的功能. 内部实现 在Go语言里,数组是一个长度固定的数据类型,用于存储一段具有 ...

  8. c++ static_cast和dynamic_cast详解

    注:从图中可以看出,派生类不仅有自己的方法和属性,同时它还包括从父类继承来的方法和属性.当我们从派生类向基类转换时,不管用传统的c语言还是c++转换方式都可以百分百转换成功.但是可怕是向下转换类型,也 ...

  9. PAT B1041 考试座位号(15)

    解题要点: 使用结构体保存准考证号,考试座位号 试机座位号作考生数组下标 通过试机座位号获取考生号,座位号 考生号使用long long存放 //课本AC代码 #include <cstdio& ...

  10. LeetCode 19——删除链表的倒数第N个节点(JAVA)

    给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点. 示例: 给定一个链表: 1->2->3->4->5, 和 n = 2. 当删除了倒数第二个节点后,链表变为 ...