调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!

博客前端积累文档公众号GitHub


目录

  1. 数据结构:栈
  2. 调用栈是什么?用来做什么?
  3. 调用栈的运行机制
  4. 调用栈优化内存
  5. 调用栈debug大法

数据结构:栈

栈是一种遵从后进先出(LIFO)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。

生活中的栗子,帮助一下理解:

餐厅里面堆放的盘子(栈),一开始放的都在下面(先进),后面放的都在上面(后进),洗盘子的时候先从上面开始洗(先出)。

调用栈是什么?用来做什么?

  1. 调用栈是一种栈结构的数据,它是由调用侦组成的
  2. 调用栈记录了函数的执行顺序和函数内部变量等信息

调用栈的运行机制

机制

程序运行到一个函数,它就会将其添加到调用栈中,当从这个函数返回的时候,就会将这个函数从调用栈中删掉。

看一下例子帮助理解:

  1. // 调用栈中的执行步骤用数字表示
  2. printSquare(5); // 1 添加
  3. function printSquare(x) {
  4. var s = multiply(x, x); // 2 添加 => 3 运行完成,内部没有再调用其他函数,删掉
  5. console.log(s); // 4 添加 => 5 删掉
  6. // 运行完成 删掉printSquare
  7. }
  8. function multiply(x, y) {
  9. return x * y;
  10. }

调用栈中的执行步骤如下(删除multiply的步骤被省略了):

调用侦

每个进入到调用栈中的函数,都会分配到一个单独的栈空间,称为“调用侦”。

在调用栈中每个“调用侦”都对应一个函数,最上方的调用帧称为“当前帧”,调用栈是由所有的调用侦形成的。

找到一张图片,调用侦:

调用栈优化内存

调用栈的内存消耗

如上图,函数的变量等信息会被调用侦保存起来,所以调用侦中的变量不会被垃圾收集器回收

当函数嵌套的层级比较深了,调用栈中的调用侦比较多的时候,这些信息对内存消耗是非常大的。

针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,ES6提供了“尾调用优化”来解决调用侦过多,引起的内存消耗过大的问题。

何谓尾调用

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

  1. function f(x){
  2. return g(x); // 最后一步调用另一个函数并且使用return
  3. }
  4. function f(x){
  5. g(x); // 没有return 不算尾调用 因为不知道后面还有没有操作
  6. // return undefined; // 隐式的return
  7. }

尾调用优化优化了什么?

尾调用用来删除外层无用的调用侦,只保留内层函数的调用侦,来节省浏览器的内存。

下面这个例子调用栈中的调用侦一直只有一项,如果不使用尾调用的话会出现三个调用侦:

  1. a() // 1 添加a到调用栈
  2. function a(){
  3. return b(); // 在调用栈中删除a 添加b
  4. }
  5. function b(){
  6. return c() // 删除b 添加c
  7. }

防止爆栈

浏览器对调用栈都有大小限制,在ES6之前递归比较深的话,很容易出现“爆栈”问题(stack overflow)。

现在可以使用“尾调用优化”来写一个“尾递归”,只保存一个调用侦,来防止爆栈问题。

注意

  1. 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧。

如果要使用外层函数的变量,可以通过参数的形式传到内层函数中

  1. function a(){
  2. var aa = 1;
  3. let b = val => aa + val // 使用了外层函数的参数aa
  4. return b(2) // 无法进行尾调用优化
  5. }
  1. 尾调用优化只在严格模式下开启,非严格模式是无效的。
  2. 如果环境不支持“尾调用优化”,代码还可以正常运行,是无害的!

更多

关于尾递归以及更多尾调用优化的内容,推荐查阅ES6入门-阮一峰

调用栈debug大法

查看调用栈有什么用

  1. 查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。

  2. 快速定位问题/修改三方库的代码。

    当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。

    通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。

如何查看调用栈

  1. 只查看调用栈:console.trace
  1. a()
  2. function a() {
  3. b();
  4. }
  5. function b() {
  6. c()
  7. }
  8. function c() {
  9. let aa = 1;
  10. console.trace()
  11. }

如图所示,点击右侧还能查看代码位置:

  1. bugger打断点形式,这也是我最喜欢的调试方式:

积跬步以至千里

平时需要有意识的去做这种小的优化(我现在就是),尽量写最佳实践的代码。

项目小的时候可能没什么影响,当一个项目体量大的时候,尤其是一些小方法拼接嵌套成一个大的API输出时,这时调用栈中对内存的消耗将是巨大的!这种优化也是不可小觑的,积跬步以至千里,诸君共勉!

结语

本文主要讲了这几个方面的内容:

  1. 理解调用栈的运行机制,对代码背后的一些执行机制也可以更加了解,帮助我们在百尺竿头更进一步。
  2. 我们应该在日常的code中,有意识的使用ES6的“尾调用优化”,来减少调用栈的长度,节省客户端内存。
  3. 利用调用栈,对第三方库或者不熟悉的项目,可以更快速的定位问题,提高我们debug速度。

最后:之前写过一篇关于垃圾回收机制与内存泄露的文章,感兴趣的同学可以扩展一下。

如果这篇文章帮助到了你,欢迎点赞和关注,你的支持是对我最大的鼓励!

博客前端积累文档公众号GitHub

以上2019/5/19

参考资料:

JS垃圾回收机制与常见内存泄露的解决方法

ES6入门-阮一峰

JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

浅析javascript调用栈

js 调用栈机制与ES6尾调用优化介绍的更多相关文章

  1. JS魔法堂:ES6新特性——GeneratorFunction介绍

    一.前言       第一次看koajs的示例时,发现该语句 function *(next){...............} ,这是啥啊?于是搜索一下,原来这是就是ES6的新特性Generator ...

  2. JS的递归与TCO尾调用优化

    转自:https://segmentfault.com/a/1190000004018047 这两天搜了下JS递归的相关文章, 觉得这篇文章很不错, 就顺手翻译了下,也算给自己做个笔记,题目是我自己加 ...

  3. JavaScript中的尾调用优化

    文章来源自:http://www.zhufengpeixun.com/qianduanjishuziliao/javaScriptzhuanti/2017-08-08/768.html JavaScr ...

  4. JavaScript 中的尾调用

    尾调用(Tail Call) 尾调用是函数式编程里比较重要的一个概念,它的意思是在函数的执行过程中,如果最后一个动作是一个函数的调用,即这个调用的返回值被当前函数直接返回,则称为尾调用,如下所示: f ...

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

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

  6. Lua 正确的尾调用(proper tail call)

    Lua支持“尾调用消除(tail-call elimination)”.尾调用(tail call):当一个函数调用是另一个函数的最后一个动作时,该调用才算是一条“尾调用”.例如,下面的代码就是一条“ ...

  7. 前端项目中常用es6知识总结 -- 箭头函数及this指向、尾调用优化

    项目开发中一些常用的es6知识,主要是为以后分享小程序开发.node+koa项目开发以及vueSSR(vue服务端渲染)做个前置铺垫. 项目开发常用es6介绍 1.块级作用域 let const 2. ...

  8. ES6躬行记(15)——箭头函数和尾调用优化

    一.箭头函数 箭头函数(Arrow Function)是ES6提供的一个很实用的新功能,与普通函数相比,不但在语法上更为简洁,而且在使用时也有更多注意点,下面列出了其中的三点: (1)由于不能作为构造 ...

  9. ES6学习笔记 -- 尾调用优化

    什么是尾调用? 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数. function f(x) { return g(x) } 如上,函数 f 的最后一 ...

随机推荐

  1. bvlc_reference_caffenet.caffemodel

    #uncoding:utf-8 # set up Python environment: numpy for numerical routines, and matplotlib for plotti ...

  2. Struts2拦截器 解决登录问题

    一.了解Struts2 拦截器[Interceptor] 拦截器的工作原理如图  拦截器是由每一个action请求(request)都包装在一系列的拦截器的内部,通过redirectAction再一次 ...

  3. 【BZOJ4950】lydsy七月月赛 C 二分图最大匹配

    [BZOJ4950]lydsy七月月赛 C 题面 题解:比较直接的想法就是:每行,每列的最大值都留下,剩下的格子都变成1.但是如果一个格子既是行的最大值又是列的最大值,那么我们只需要把它留下即可.这就 ...

  4. GitLab Pages expect to run on their own virtual host

    GitLab Pages administration | GitLab https://docs.gitlab.com/ce/administration/pages/

  5. java 图形化界面 布局管理器

    package Layout; import java.awt.*; import javax.swing.*; public class MyBorderLayout extends JFrame{ ...

  6. CoreGraphics(转)

    2.CoreGraphics 上面我们讲过,UIBezierPath是CoreGraphics的封装,使用它可以完成大部分的绘图操作,不过更底层的CoreGraphics更加强大. CoreGraph ...

  7. leetcode 748. Shortest Completing Word

    Find the minimum length word from a given dictionary words, which has all the letters from the strin ...

  8. 【React系列】Props 验证

    Props 验证使用 propTypes,它可以保证我们的应用组件被正确使用,React.PropTypes 提供很多验证器 (validator) 来验证传入数据是否有效.当向 props 传入无效 ...

  9. hdu1078 FatMouse and Cheese —— 记忆化搜索

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1078 代码1: #include<stdio.h>//hdu 1078 记忆化搜索 #in ...

  10. 使用eclipse的SVN连接码云

    码云配置: 码云的项目上,启用SVN访问 eclipse的配置,不配置这个会报错