js尾调用优化的实现


Posted in Javascript onMay 23, 2019

尾调用(Tail Call)是函数式编程的一个重要概念,本文介绍它的含义和用法。

一、什么是尾调用?

尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){
 return g(x);
}

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

以下两种情况,都不属于尾调用。

// 情况一
function f(x){
 let y = g(x);
 return y;
}

// 情况二
function f(x){
 return g(x) + 1;
}

上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function f(x) {
 if (x > 0) {
  return m(x)
 }
 return n(x);
}

上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

二、尾调用优化

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

我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。

js尾调用优化的实现

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

function f() {
 let m = 1;
 let n = 2;
 return g(m + n);
}
f();

// 等同于
function f() {
 return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。

这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

三、尾递归

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

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

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

factorial(5) // 120

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

如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

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

factorial(5, 1) // 120

js尾调用优化的实现

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

四、递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?

两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

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

function factorial(n) {
 return tailFactorial(n, 1);
}

factorial(5) // 120

上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

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

上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 。

第二种方法就简单多了,就是采用ES6的函数默认值。

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

factorial(5) // 120

上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。

总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持"尾调用优化"的语言(比如Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

([说明] 本文摘自我写的《ECMAScript 6入门》)

五、严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js 格式化时间日期函数小结
Mar 20 Javascript
Javascript实现CheckBox的全选与取消全选的代码
Jul 20 Javascript
javascript实现相同事件名称,不同命名空间的调用方法
Jun 26 Javascript
简单实现兼容各大浏览器的js复制内容到剪切板
Sep 09 Javascript
详解JavaScript中基于原型prototype的继承特性
May 05 Javascript
利用Jquery队列实现根据输入数量显示的动画
Sep 01 Javascript
vue项目中公用footer组件底部位置的适配问题
May 10 Javascript
Vue项目中跨域问题解决方案
Jun 05 Javascript
JS 实现获取验证码 倒计时功能
Oct 29 Javascript
jQuery实现上下滚动公告栏详细代码
Nov 21 jQuery
微信小程序 image组件遇到的问题
May 28 Javascript
js抽奖转盘实现方法分析
May 16 Javascript
浅谈redux, koa, express 中间件实现对比解析
May 23 #Javascript
Express结合Webpack的全栈自动刷新
May 23 #Javascript
ajax跨域访问遇到的问题及解决方案
May 23 #Javascript
简单了解JavaScript异步
May 23 #Javascript
vue项目添加多页面配置的步骤详解
May 22 #Javascript
vue elementUI table 自定义表头和行合并的实例代码
May 22 #Javascript
微信小程序使用websocket通讯的demo,含前后端代码,亲测可用
May 22 #Javascript
You might like
PHP常用技术文之文件操作和目录操作总结
2014/09/27 PHP
PHP版QQ互联OAuth示例代码分享
2015/07/05 PHP
JavaScript关于select的相关操作说明
2010/01/13 Javascript
页面只能打开一次Cooike如何实现
2012/12/04 Javascript
javaScript让文本框内的最后一个文字的后面获得焦点实现代码
2013/01/06 Javascript
左侧是表头的JS表格控件(自写,网上没有的)
2013/06/04 Javascript
js中onload与onunload的使用示例
2013/08/25 Javascript
JavaScript DOM 编程艺术(第2版)读书笔记(JavaScript的最佳实践)
2013/10/01 Javascript
IE下双击checkbox反应延迟问题的解决方法
2014/03/27 Javascript
js实现每日自动换一张图片的方法
2015/05/04 Javascript
javascript基于DOM实现权限选择实例分析
2015/05/14 Javascript
前端设计师们最常用的JS代码汇总
2016/09/25 Javascript
原生JS中slice()方法和splice()区别
2017/03/06 Javascript
VUE页面中加载外部HTML的示例代码
2017/09/20 Javascript
jQuery访问浏览器本地存储cookie、localStorage和sessionStorage的基本用法
2017/10/20 jQuery
JavaScript满天星导航栏实现方法
2018/03/08 Javascript
详解Vue.js中.native修饰符
2018/04/24 Javascript
vue 音乐App QQ音乐搜索列表最新接口跨域设置方法
2018/09/25 Javascript
图文详解vue框架安装步骤
2019/02/12 Javascript
详解小程序循环require之坑
2019/03/08 Javascript
开源一个微信小程序仪表盘组件过程解析
2019/07/30 Javascript
使用Webpack提升Vue.js应用程序的4种方法(翻译)
2019/10/09 Javascript
uniapp 仿微信的右边下拉选择弹出框的实现代码
2020/07/12 Javascript
[54:56]DOTA2上海特级锦标赛主赛事日 - 5 总决赛Liquid VS Secret第三局
2016/03/06 DOTA
python实现AES和RSA加解密的方法
2019/03/28 Python
在Python中使用filter去除列表中值为假及空字符串的例子
2019/11/18 Python
英国在线女鞋目的地:SIMMI
2018/12/27 全球购物
护士的自我鉴定
2014/02/07 职场文书
共产党员岗位承诺书
2014/05/29 职场文书
优秀党员自我评价范文
2014/09/15 职场文书
2014年大学教师工作总结
2014/12/02 职场文书
2016党校学习心得体会范文
2016/01/07 职场文书
85句关于理想的名言警句大全
2019/08/22 职场文书
重温经典:乔布斯在斯坦福大学的毕业演讲(双语)
2019/08/26 职场文书
python实现层次聚类的方法
2021/11/01 Python
java.util.NoSuchElementException原因及两种解决方法
2022/06/28 Java/Android