尾调用

1. 定义

尾调用是函数式编程中一个很重要的概念,当一个函数执行时的最后一个步骤是返回另一个函数的调用,这就叫做尾调用。

注意这里函数的调用方式是无所谓的,以下方式均可:

函数调用: func(···)

方法调用: obj.method(···)

call调用: func.call(···)

apply调用: func.apply(···)

并且只有下列表达式会包含尾调用:

条件操作符: ? :

逻辑或: ||

逻辑与: &&

逗号: ,

依次举例:

const a = x => x ? f() : g();

// f() 和 g() 都在尾部。

const a = () => f() || g();

// g()有可能是尾调用,f()不是

// 因为上述写法和下面的写法等效:

const a = () => {

const fResult = f(); // not a tail call

if (fResult) {

return fResult;

} else {

return g(); // tail call

}

}

// 只有当f()的结果为falsey的时候,g()才是尾调用

const a = () => f() && g();

// g()有可能是尾调用,f()不是

// 因为上述写法和下面的写法等效:

const a = () => {

const fResult = f(); // not a tail call

if (fResult) {

return g(); // tail call

} else {

return fResult;

}

}

// 只有当f()的结果为truthy的时候,g()才是尾调用

const a = () => (f() , g());

// g()是尾调用

// 因为上述写法和下面的写法等效:

const a = () => {

f();

return g();

}

2. 尾调用优化

函数在调用的时候会在调用栈(call stack)中存有记录,每一条记录叫做一个调用帧(call frame),每调用一个函数,就向栈中push一条记录,函数执行结束后依次向外弹出,直到清空调用栈,参考下图:

function foo () { console.log(111); }

function bar () { foo(); }

function baz () { bar(); }

baz();

造成这种结果是因为每个函数在调用另一个函数的时候,并没有 return 该调用,所以JS引擎会认为你还没有执行完,会保留你的调用帧。

baz() 里面调用了 bar() 函数,并没有 return 该调用,所以在调用栈中保持自己的调用帧,同时 bar() 函数的调用帧在调用栈中生成,同理,bar() 函数又调用了 foo() 函数,最后执行到 foo() 函数的时候,没有再调用其他函数,这里没有显示声明 return,所以这里默认 return undefined

foo() 执行完了,销毁调用栈中自己的记录,依次销毁 bar()baz() 的调用帧,最后完成整个流程。

如果对上面的例子做如下修改:

function foo () { console.log(111); }

function bar () { return foo(); }

function baz () { return bar(); }

baz();

这里要注意:尾调用优化只在严格模式下有效。

在非严格模式下,大多数引擎会包含下面两个属性,以便开发者检查调用栈:

  • func.arguments: 表示对 func最近一次调用所包含的参数

  • func.caller: 引用对 func最近一次调用的那个函数

在尾调用优化中,这些属性不再有用,因为相关的信息可能以及被移除了。因此,严格模式(strict mode)禁止这些属性,并且尾调用优化只在严格模式下有效。

如果尾调用优化生效,流程图就会变成这样:

我们可以很清楚的看到,尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,只要直接用内层函数的调用记录取代外层函数的调用记录就可以了,调用栈中始终只保持了一条调用帧。

这就叫做尾调用优化,如果所有的函数都是尾调用的话,那么在调用栈中的调用帧始终只有一条,这样会节省很大一部分的内存,这也是尾调用优化的意义

尾递归

1. 定义

先来看一下递归,当一个函数调用自身,就叫做递归。

function foo () {

foo();

}

上面这个操作就叫做递归,但是注意了,这里没有结束条件,是死递归,所以会报栈溢出错误的,写代码时千万注意给递归添加结束条件。

那么什么是尾递归?前面我们知道了尾调用的概念,当一个函数尾调用自身,就叫做尾递归

function foo () {

return foo();

}

2. 作用

那么尾递归相比递归而言,有哪些不同呢?我们通过下面这个求阶乘的例子来看一下:

function factorial (num) {

if (num === 1) return 1;

return num * factorial(num - 1);

}

factorial(5); // 120

factorial(10); // 3628800

factorial(500000); // Uncaught RangeError: Maximum call stack size exceeded

上面是使用递归来计算阶乘的例子,操作系统为JS引擎调用栈分配的内存是有大小限制的,如果计算的数字足够大,超出了内存最大范围,就会出现栈溢出错误。

这里500000并不是临界值,只是我用了一个足够造成栈溢出的数。

如果用尾递归来计算阶乘呢?

'use strict';

function factorial (num, total) {

if (num === 1) return total;

return factorial(num - 1, num * total);

}

factorial(5, 1); // 120

factorial(10, 1); // 3628800

factorial(500000, 1); // 分情况

// 注意,虽然说这里启用了严格模式,但是经测试,在Chrome和Firefox下,还是会报栈溢出错误,并没有进行尾调用优化

// Safari浏览器进行了尾调用优化,factorial(500000, 1)结果为Infinity,因为结果超出了JS可表示的数字范围

// 如果在node v6版本下执行,需要加--harmony_tailcalls参数,node --harmony_tailcalls test.js

// node最新版本已经移除了--harmony_tailcalls功能

通过尾递归,我们把复杂度从O(n)降低到了O(1),如果数据足够大的话,会节省很多的计算时间。由此可见,尾调用优化对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。

避免改写递归函数

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。要做到这一点,需要把函数内部所有用到的中间变量改写为函数的参数,就像上面的factorial()函数改写一样。

这样做的缺点就是语义不明显,要计算阶乘的函数,为什么还要另外传入一个参数叫total?解决这个问题的办法有两个:

1. ES6参数默认值

'use strict';

function factorial (num, total = 1) {

if (num === 1) return total;

return factorial(num - 1, num * total);

}

factorial(5); // 120

factorial(10); // 3628800

2. 用一个符合语义的函数去调用改写后的尾递归函数

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

function factorial (num) {

return tailFactorial(num, 1);

}

factorial(5); // 120

factorial(10); // 3628800

上面这种写法其实有点类似于做了一个函数柯里化,但不完全符合柯里化的概念。函数柯里化是指把接受多个参数的函数转换为接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下参数且返回结果的新函数。

概念看着很绕口,我们来个例子感受一下:

// 普通加法函数

function add (x, y, z) {

return x + y + z;

}

add(1, 2, 3); // 6

// 改写为柯里化加法函数

function add (x) {

return function (y) {

return function (z) {

return x + y + z;

}

}

}

add(1)(2)(3); // 6

可以看到,柯里化函数通过闭包找到父作用域里的变量,最后依次相加输出结果。通过这个例子,可能看不出为什么要用柯里化,有什么好处,这个我们以后再谈,这里先引出一个概念。

是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

如果用柯里化改写求阶乘的例子:

// 柯里化函数

function curry (fn) {

var _fnArgLength = fn.length;

function wrap (...args) {

var _args = args;

var _argLength = _args.length;

// 如果传的是所有参数,直接返回fn调用

if (_fnArgLength === _argLength) {

return fn.apply(null, args);

}

function act (...args) {

_args = _args.concat(args);

if (_args.length === _fnArgLength) {

return fn.apply(null, _args);

}

return act;

}

return act;

}

return wrap;

}

// 尾递归函数

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

// 改写

var factorial = curry(tailFactorial);

factorial(5)(1); // 120

factorial(10)(1); // 3628800

这是符合柯里化概念的写法,在阮一峰老师的文章中是这样写的:

function currying(fn, n) {

return function (m) {

return fn.call(this, m, n);

};

}

function tailFactorial(n, total) {

if (n === 1) return total;

return tailFactorial(n - 1, n * total);

}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

我个人认为,这种写法其实不是柯里化,因为并没有将多参数的tailFacrotial改写为接受单参数的形式,只是换了一种写法,和下面这样写意义是一样的:

function factorial (num) {

return tailFactorial(num, 1);

}

function tailFactorial (num, total) {

if (num === 1) return total;

return tailFactorial(num - 1, num * total);

}

factorial(5); // 120

factorial(10); // 3628800

结束

这篇文章我们主要讨论了尾调用优化和柯里化。要注意的是,经过测试,Chrome和Firefox并没有对尾调用进行优化,Safari对尾调用进行了优化。Node高版本也已经去除了通过--harmony_tailcalls参数启用尾调用优化。

有任何问题,欢迎大家留言讨论,另附我的博客网站,快来呀~~

欢迎关注我的公众号

参考链接

http://www.ruanyifeng.com/blog/2015/04/tail-call.html https://juejin.im/post/6844903544756125704 https://github.com/lamdu/lamdu/issues/90

ondestroy什么时候调用_尾调用和尾递归相关推荐

  1. mockito 多层调用_连续调用的Mockito迭代器样式存根

    mockito 多层调用 Sometimes we want to mock different responses for the consecutive calls on the same met ...

  2. 尾调用优化 java_基于Java8函数式编程求一个List的全部子集|尾调用优化解决递归性能问题...

    目录 基于函数式编程求一个List的全部子集 代码来自<Java8 in Action>,思路和其他递归解决方法一致,但不同的地方在concat方法 public static List& ...

  3. ES6 尾调用和尾递归优化

    尾调用 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数. function fun(x){return a(x); } 上面代码中,函数fun的最后一 ...

  4. lua--函数深入:闭合函数,局部函数,尾调用

    lua函数具有两大特征:函数作为第一类值,函数具有特定的词法域(Lexical Scoping) 所谓第一类值:代表函数和其他传统类型的值是等价的(例如数字和字符串),函数可以同他们一样存储在变量,t ...

  5. 递归循环一个无限极数组_理解递归、尾调用优化和蹦床函数优化

    想要理解递归,您必须先理解递归.开个玩笑罢了, 递归 是一种编程技巧,它可以让函数在不使用 for 或 while 的情况下,使用一个调用自身的函数来实现循环. 例子 1:整数总和 例如,假设我们想要 ...

  6. Lua 函数、闭包、尾调用总结

    <lua 程序设计>在线阅读:http://book.luaer.cn/ 1.函数 函数有两种用途: 完成指定的任务,这种情况下函数作为调用语句使用: 计算并返回值,这种情况下函数作为赋值 ...

  7. python十八:尾调用与递归

    递归,众所周知,很耗计算机性能.但把递归改写成尾调用,就会很好的节省内存. 尾调用:就是在函数的最后一步调用. def foo(x):x += 1return x# 不是尾调用,因为最后一步,是 fo ...

  8. jsp调用controller方法_RPC调用_服务注册与发现

    RPC调用_单体架构_SOA架构 系统架构的演变 1 传统的单体架构 1.1 什么是单体架构 一个归档包(例如 war 格式或者 Jar 格式)包含了应用所有功能的应用程序,我们通常称之 为单体应用. ...

  9. [js] 举例说明js中什么是尾调用优化

    [js] 举例说明js中什么是尾调用优化 写在前面 上次介绍了什么是尾调用以及怎么准确快速的判别一个函数调用是否为尾调用.那么,我们判别尾调用的意义是什么呢?做什么事情总归有个目的,那么今天我们就来系 ...

最新文章

  1. VTK:PolyData之CellsInsideObject
  2. c++容器共性机制研究
  3. Spring整合了CXF的一个applicationContext.xml的配置文件
  4. 云上故事 | “电”亮数字生活,阿里云助力南方电网智能调度
  5. html js css如何关联_会html+css+js就能把前端项目发布到多个平台
  6. Enterprise Library—缓存应用程序块
  7. Eclipse-无法引用maven依赖的类/没有MavenDependencies/没有buildpath
  8. linux系统升级后,手动编译的kernel无法启动问题
  9. Https 真的安全吗?可以抓包吗?如何防止抓包吗?
  10. python3的fft_FFT乘法Python 3.4.3
  11. 【C语言】C语言读取文本文件
  12. PX4 编译报错问题解决方法、PX4切换固定版本编译
  13. 神经网络与机器学习笔记
  14. 机器学习面试题(上)
  15. Redis - Windows下载与安装
  16. 第三方android 模拟器哪个好用吗,PC安卓模拟器哪个好用 电脑手游助手模拟器测评排行...
  17. 航天二院计算机硕士待遇怎么样,航天二院706所
  18. 小白也能做的选择(上)
  19. 京东市值达4600亿元创历史新高
  20. python程序论文答辩_【干货】毕业论文的答辩流程及注意事项

热门文章

  1. asp.net 六大对象之Request、Response
  2. 属性页中的ON_UPDATE_COMMAND_UI
  3. 字符串大小写字母转换c 语言,towlower()
  4. U-Boot启动流程详解
  5. 全国计算机等级考试题库二级C操作题100套(第97套)
  6. 全国计算机等级考试题库二级C操作题100套(第20套)
  7. tp5 php7 报500,记一次TP单元测试报500错误的问题
  8. rh php56 php,在全球范围内提供RHSCL PHP的最佳方法
  9. 执行git命令时提示秘钥权限太开放‘Permissions 0644 for ‘/Users/liuml/.ssh/id_rsa_tz‘ are too open.’
  10. 软件测试人员:如何优秀的提Bug?