详解JavaScript调用栈、尾递归和手动优化


Posted in Javascript onJune 03, 2017

调用栈(Call Stack)

调用栈(Call Stack)是一个基本的计算机概念,这里引入一个概念:栈帧。

栈帧是指为一个函数调用单独分配的那部分栈空间。

当运行的程序从当前函数调用另外一个函数时,就会为下一个函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前帧。而原来的函数也有一个对应的栈帧,被称为调用帧。每一个栈帧里面都会存入当前函数的局部变量。

详解JavaScript调用栈、尾递归和手动优化

当函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数。并将程序运行权利(帧指针)交给此时栈顶的栈帧。这种后进后出的结构也就是函数的调用栈。

而在JavaScript里,可以很方便的通过console.trace()这个方法查看当前函数的调用帧

详解JavaScript调用栈、尾递归和手动优化

尾调用

说尾递归之前必须先了解一下什么是尾调用。简单的说,就是一个函数执行的最后一步是将另外一个函数调用并返回。

以下是正确示范:

// 尾调用正确示范1.0
function f(x){
 return g(x);
}

// 尾调用正确示范2.0
function f(x) {
 if (x > 0) {
  return m(x)
 }
 return n(x);
}

1.0程序的最后一步即是执行函数g,同时将其返回值返回。2.0中,尾调用并不是非得写在最后一行中,只要执行时,是最后一步操作就可以了。

以下是错误示范:

// 尾调用错误示范1.0
function f(x){
 let y = g(x);
 return y;
}

// 尾调用错误示范2.0
function f(x){
 return g(x) + 1;
}
// 尾调用错误示范3.0
function f(x) {
 g(x); // 这一步相当于g(x) return undefined
}

1.0最后一步为赋值操作,2.0最后一步为加法运算操作,3.0隐式的有一句return undefined

尾调用优化

在调用栈的部分我们知道,当一个函数A调用另外一个函数B时,就会形成栈帧,在调用栈内同时存在调用帧A和当前帧B,这是因为当函数B执行完成后,还需要将执行权返回A,那么函数A内部的变量,调用函数B的位置等信息都必须保存在调用帧A中。不然,当函数B执行完继续执行函数A时,就会乱套。

那么现在,我们将函数B放到了函数A的最后一步调用(即尾调用),那还有必要保留函数A的栈帧么?当然不用,因为之后并不会再用到其调用位置、内部变量。因此直接用函数B的栈帧取代A的栈帧即可。当然,如果内层函数使用了外层函数的变量,那么就仍然需要保留函数A的栈帧,典型例子即是闭包。

在网上有很多关于讲解尾调用的博客文章,其中流传广泛的一篇中有这样一段。我不是很认同。

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) 的调用记录。

但我认为第一种中,也是先执行m+n这步操作,再调用函数g同时返回。这应当是一次尾调用。同时m+n的值也通过参数传入函数g内部,并没有直接引用,因此也不能说需要保存f内部的变量的值。

总得来说,如果所有函数的调用都是尾调用,那么调用栈的长度就会小很多,这样需要占用的内存也会大大减少。这就是尾调用优化的含义。

尾递归

递归,是指在函数的定义中使用函数自身的一种方法。函数调用自身即称为递归,那么函数在尾调用自身,即称为尾递归。

最常见的递归,斐波拉契数列,普通递归的写法:

function f(n) {
 if (n === 0 || n === 1) return n 
 else return f(n - 1) + f(n - 2)
}

这种写法,简单粗暴,但是有个很严重的问题。调用栈随着n的增加而线性增加,当n为一个大数(我测了一下,当n为100的时候,浏览器窗口就会卡死。。)时,就会爆栈了(栈溢出,stack overflow)。这是因为这种递归操作中,同时保存了大量的栈帧,调用栈非常长,消耗了巨大的内存。

接下来,将普通递归升级为尾递归看看。

function fTail(n, a = 0, b = 1) { 
 if (n === 0) return a
 return fTail(n - 1, b, a + b)
}

很明显,其调用栈为

fTail(5) => fTail(4, 1, 1) => fTail(3, 1, 2) => fTail(2, 2, 3) => fTail(1, 3, 5) => fTail(0, 5, 8) => return 5

被尾递归改写之后的调用栈永远都是更新当前的栈帧而已,这样就完全避免了爆栈的危险。

但是,想法是好的,从尾调用优化到尾递归优化的出发点也没错,然并卵:),让我们看看V8引擎官方团队的解释

Proper tail calls have been implemented but not yet shipped given that a change to the feature is currently under discussion at TC39.

意思就是人家已经做好了,但是就是还不能不给你用:)嗨呀,好气喔。

当然,人家肯定是有他的正当理由的:

  1. 在引擎层面消除尾递归是一个隐式的行为,程序员写代码时可能意识不到自己写了死循环的尾递归,而出现死循环后又不会报出stack overflow的错误,难以辨别。
  2. 堆栈信息会在优化的过程中丢失,开发者调试非常困难。

道理我都懂,但是不信邪的我拿nodeJs(v6.9.5)手动测试了一下:

详解JavaScript调用栈、尾递归和手动优化

好的,我服了

手动优化

虽然我们暂时用不上ES6的尾递归高端优化,但递归优化的本质还是为了减少调用栈,避免内存占用过多,爆栈的危险。而俗话说的好,一切能用递归写的函数,都能用循环写——尼克拉斯·夏,如果将递归改成循环的话,不就解决了这种调用栈的问题么。

方案一:直接改函数内部,循环执行

function fLoop(n, a = 0, b = 1) { 
 while (n--) {
  [a, b] = [b, a + b]
 }
 return a
}

这种方案简单粗暴,缺点就是没有递归的那种写法比较容易理解。

方案二:Trampolining(蹦床函数)

function trampoline(f) { 
 while (f && f instanceof Function) {
  f = f()
 }
 return f
}

function f(n, a = 0, b = 1) { 
 if (n > 0) {
  [a, b] = [b, a + b]
  return f.bind(null, n - 1, a, b)
 } else {
  return a
 }
}

trampoline(f(5)) // return 5

这种写法算是容易理解一些了,就是蹦床函数的作用需要仔细看看。缺点还有就是需要修改原函数内部的写法。

方案三:尾递归函数转循环方法

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

const f = tailCallOptimize(function(n, a = 0, b = 1) { 
 if (n === 0) return a
 return f(n - 1, b, a + b)
})
f(5) // return 5

经过 tailCallOptimize 包装后返回的是一个新函数 accumulator,执行 f时实际执行的是这个函数。这种方法可以不用修改原递归函数,当调用递归时只用使用该方法转置一下便可解决递归调用的问题。

总结

尾递归优化是个好东西,但既然暂时用不上,那我们就该在平时编码的过程中,对使用到了递归的地方特别敏感,时刻避免出现死循环,爆栈等危险。毕竟,好的工具不如好的习惯。

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

Javascript 相关文章推荐
Javascript - HTML的request类
Jan 09 Javascript
js中匿名函数的N种写法
Sep 08 Javascript
JQuery获取当前屏幕的高度宽度的实现代码
Jul 12 Javascript
javascript中常用编程知识
Apr 08 Javascript
jquery 删除字符串最后一个字符的方法解析
Feb 11 Javascript
javascript生成随机大小写字母的方法
Feb 20 Javascript
javascript中caller和callee详解
Aug 10 Javascript
使用JavaScript脚本判断页面是否在微信中被打开
Mar 06 Javascript
深入理解javascript作用域第二篇之词法作用域和动态作用域
Jul 24 Javascript
javascript insertAfter()定义与用法示例
Jul 25 Javascript
JAVA中截取字符串substring用法详解
Apr 14 Javascript
vue2.0自定义指令示例代码详解
Apr 25 Javascript
详解有关easyUI的拖动操作中droppable,draggable用法例子
Jun 03 #Javascript
利用vueJs实现图片轮播实例代码
Jun 03 #Javascript
angular中使用Socket.io实例代码
Jun 03 #Javascript
jquery请求servlet实现ajax异步请求的示例
Jun 03 #jQuery
深入理解Node中的buffer模块
Jun 03 #Javascript
MvcPager分页控件 适用于Bootstrap
Jun 03 #Javascript
全面解析Node.js 8 重要功能和修复
Jun 02 #Javascript
You might like
PHP取进制余数函数代码
2012/01/19 PHP
php自定义session示例分享
2014/04/22 PHP
用 Composer构建自己的 PHP 框架之构建路由
2014/10/30 PHP
Laravel程序架构设计思路之使用动作类
2018/06/07 PHP
PHP实现单例模式建立数据库连接的方法分析
2020/02/11 PHP
js调用打印机打印网页字体总是缩小一号的解决方法
2014/01/24 Javascript
Jquery实现侧边栏跟随滚动条固定(兼容IE6)
2014/04/02 Javascript
javascript 自定义回调函数示例代码
2014/09/26 Javascript
个人总结的一些JavaScript技巧、实用函数、简洁方法、编程细节
2015/06/10 Javascript
js点击按钮实现水波纹效果代码(CSS3和Canves)
2016/09/15 Javascript
Jquery on绑定的事件 触发多次实例代码
2016/12/08 Javascript
浅析node应用的timing-attack安全漏洞
2018/02/28 Javascript
JS中数据结构之栈
2019/01/01 Javascript
JavaScript格式化json和xml的方法示例
2019/01/22 Javascript
vue实现图片上传功能
2020/05/28 Javascript
python发送伪造的arp请求
2014/01/09 Python
python实现带验证码网站的自动登陆实现代码
2015/01/12 Python
python pandas修改列属性的方法详解
2018/06/09 Python
使用python的pyplot绘制函数实例
2020/02/13 Python
Python安装与卸载流程详细步骤(图解)
2020/02/20 Python
tensorflow 20:搭网络,导出模型,运行模型的实例
2020/05/26 Python
Python3如何使用tabulate打印数据
2020/09/25 Python
利用Python实现最小二乘法与梯度下降算法
2021/02/21 Python
美国电力供应商店/电气批发商:USESI
2018/10/12 全球购物
Perfume’s Club德国官网:在线购买香水
2019/04/08 全球购物
仓库管理制度
2014/01/21 职场文书
社区工作感言
2014/02/21 职场文书
如何写一份好的英文求职信
2014/03/19 职场文书
热门专业求职信
2014/05/24 职场文书
国际商务英语专业求职信
2014/07/08 职场文书
培训通知书模板
2015/04/17 职场文书
2015年班干部工作总结
2015/04/29 职场文书
2016新教师岗前培训心得体会
2016/01/08 职场文书
python数字转对应中文的方法总结
2021/08/02 Python
JMeter对MySQL数据库进行压力测试的实现步骤
2022/01/22 MySQL
mysql分组后合并显示一个字段的多条数据方式
2022/01/22 MySQL