js 调用栈机制与ES6尾调用优化介绍
调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!
目录
- 数据结构:栈
- 调用栈是什么?用来做什么?
- 调用栈的运行机制
- 调用栈优化内存
- 调用栈debug大法
数据结构:栈
栈是一种遵从后进先出(LIFO
)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。
生活中的栗子,帮助一下理解:
餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。
调用栈是什么?用来做什么?
- 调用栈是一种栈结构的数据,它是由调用侦组成的。
- 调用栈记录了函数的执行顺序和函数内部变量等信息。
调用栈的运行机制
机制:
程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。
看一下例子帮助理解:
// 调用栈中的执行步骤用数字表示
printSquare(5); // 1 添加
function printSquare(x) {
var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉
console.log(s); // 4 添加 => 5 删掉
// 运行完成 删掉printSquare
}
function multiply(x, y) {
return x * y;
}
调用栈中的执行步骤如下(删除multiply的步骤被省略了):
调用侦:
每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。
在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。
找到一张图片,调用侦:
调用栈优化内存
调用栈的内存消耗:
如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收。
当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。
针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。
何谓尾调用:
尾调用指的是:函数的最后一步是调用另一个函数。
function f(x){
return g(x); // 最后一步调用另一个函数并且使用return
}
function f(x){
g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作
// return undefined; // 隐式的return
}
尾调用优化优化了什么?
尾调用用来删除外层无用的调用侦,只保留内层函数的调用侦,来节省浏览器的内存。
下面这个例子调用栈中的调用侦一直只有一项,如果不使用尾调用的话会出现三个调用侦:
a() // 1 添加a到调用栈
function a(){
return b(); // 在调用栈中删除a 添加b
}
function b(){
return c() // 删除b 添加c
}
防止爆栈:
浏览器对调用栈都有大小限制,在ES6之前递归比较深的话,很容易出现“爆栈”问题(stack overflow)。
现在可以使用“尾调用优化”来写一个“尾递归”,只保存一个调用侦,来防止爆栈问题。
注意:
- 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。
如果要使用外层函数的变量,可以通过参数的形式传到内层函数中
function a(){
var aa = 1;
let b = val => aa + val // 使用了外层函数的参数aa
return b(2) // 无法进行尾调用优化
}
- 尾调用优化只在严格模式下开启,非严格模式是无效的。
- 如果环境不支持“尾调用优化”,代码还可以正常运行,是无害的!
更多:
关于尾递归以及更多尾调用优化的内容,推荐查阅ES6入门-阮一峰
调用栈debug大法
查看调用栈有什么用
查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。
快速定位问题/修改三方库的代码。
当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。
通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。
如何查看调用栈
- 只查看调用栈:
console.trace
a()
function a() {
b();
}
function b() {
c()
}
function c() {
let aa = 1;
console.trace()
}
如图所示,点击右侧还能查看代码位置:
bugger
打断点形式,这也是我最喜欢的调试方式:
积跬步以至千里
平时需要有意识的去做这种小的优化(我现在就是),尽量写最佳实践的代码。
项目小的时候可能没什么影响,当一个项目体量大的时候,尤其是一些小方法拼接嵌套成一个大的API输出时,这时调用栈中对内存的消耗将是巨大的!这种优化也是不可小觑的,积跬步以至千里,诸君共勉!
结语
本文主要讲了这几个方面的内容:
- 理解调用栈的运行机制,对代码背后的一些执行机制也可以更加了解,帮助我们在百尺竿头更进一步。
- 我们应该在日常的code中,有意识的使用ES6的“尾调用优化”,来减少调用栈的长度,节省客户端内存。
- 利用调用栈,对第三方库或者不熟悉的项目,可以更快速的定位问题,提高我们debug速度。
最后:之前写过一篇关于垃圾回收机制与内存泄露的文章,感兴趣的同学可以扩展一下。
如果这篇文章帮助到了你,欢迎点赞和关注,你的支持是对我最大的鼓励!
以上2019/5/19
参考资料:
JavaScript 如何工作:对引擎、运行时、调用堆栈的概述
js 调用栈机制与ES6尾调用优化介绍的更多相关文章
- JS魔法堂:ES6新特性——GeneratorFunction介绍
一.前言 第一次看koajs的示例时,发现该语句 function *(next){...............} ,这是啥啊?于是搜索一下,原来这是就是ES6的新特性Generator ...
- JS的递归与TCO尾调用优化
转自:https://segmentfault.com/a/1190000004018047 这两天搜了下JS递归的相关文章, 觉得这篇文章很不错, 就顺手翻译了下,也算给自己做个笔记,题目是我自己加 ...
- JavaScript中的尾调用优化
文章来源自:http://www.zhufengpeixun.com/qianduanjishuziliao/javaScriptzhuanti/2017-08-08/768.html JavaScr ...
- JavaScript 中的尾调用
尾调用(Tail Call) 尾调用是函数式编程里比较重要的一个概念,它的意思是在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用,如下所示: f ...
- javascript专题系列--尾调用和尾递归
最近在看<冴羽的博客>,讲真,确实受益匪浅,已经看了javascript 深入系列和专题系列的大部分文章,可是现在才想起来做笔记.所以虽然很多以前面试被问得一脸懵逼的问题都被“一语惊醒梦中 ...
- Lua 正确的尾调用(proper tail call)
Lua支持“尾调用消除(tail-call elimination)”.尾调用(tail call):当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”.例如,下面的代码就是一条“ ...
- 前端项目中常用es6知识总结 -- 箭头函数及this指向、尾调用优化
项目开发中一些常用的es6知识,主要是为以后分享小程序开发.node+koa项目开发以及vueSSR(vue服务端渲染)做个前置铺垫. 项目开发常用es6介绍 1.块级作用域 let const 2. ...
- ES6躬行记(15)——箭头函数和尾调用优化
一.箭头函数 箭头函数(Arrow Function)是ES6提供的一个很实用的新功能,与普通函数相比,不但在语法上更为简洁,而且在使用时也有更多注意点,下面列出了其中的三点: (1)由于不能作为构造 ...
- ES6学习笔记 -- 尾调用优化
什么是尾调用? 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数. function f(x) { return g(x) } 如上,函数 f 的最后一 ...
随机推荐
- Java 多线程1(转载)
来源:http://hllvm.group.iteye.com/group/wiki/2877-synchronized-volatile 最近想将java基础的一些东西都整理整理,写下来,这是对知识 ...
- 《UNIX 网络编程 第二版》编译环境的搭建( 运行本专栏代码必读 )
第一步:搭建基本的编译环境 安装gcc, g++, bulid-essential等编译软件 第二步:下载本书示例源码包 可在这里下载http://ishare.iask.sina.com.cn/f/ ...
- LINQ to SQL 语句(1)之 Where
LINQ to SQL 语句(1)之 Where Where 操作 适用场景:实现过滤,查询等功能. 说明:与 SQL 命令中的 Where 作用相似,都是起到范围限定也就是过滤作用的 ,而判断条 件 ...
- EasyPusher RTSP直播之RTP数据包格式解析
-本篇由团队成员Fantasy供稿! RTP包头格式 码流总体结构 h264的功能分为两层,视频编码层(VCL)和网络提取层(NAL).H.264 的编码视频序列包括一系列的NAL 单元,每个NAL ...
- sort_action
li, r = [23,8, 45, 5, 0, -6, 745,8, 8], [] while (len(li) > 0): loop_, min, tag = len(li), li[0], ...
- Hadoop实战-Flume之Source interceptor(十一)(2017-05-16 22:40)
a1.sources = r1 a1.sinks = k1 a1.channels = c1 # Describe/configure the source a1.sources.r1.type = ...
- tomcat安装后问题解决
tomcat安装后问题解决 (1)tomcat无法正常启动的原因分析 JAVA_HOME 配置错误,或者没有配置 如果你的机器已经占有了8080 端口,则无法启动, 解决方法 (1) 你可以808 ...
- appium()-The event firing
原文地址:https://github.com/appium/java-client/blob/master/docs/The-event_firing.md since 4.1.0 The purp ...
- android通过DialogFragment实现时间选择
在android开发中,时间控件是不可或缺的一部分,特别是在设置个人生日或按时间进行搜索时都要用到.Android有内置的DatePicker和timePicker,使用起来也是相当的方便,既可以在布 ...
- ZOJ - 3948 Marjar Cola 【循环】
题目链接 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3948 题意 用 x 个 瓶身 可以 换 一瓶饮料 用 y 个 瓶 ...