JS 调用栈机制与 ES6 尾调用优化介绍

简介: 调用栈的英文名叫做Call Stack,大家或多或少是有听过的,但是对于js调用栈的工作方式以及如何在工作中利用这一特性,大部分人可能没有进行过更深入的研究,这块内容可以说对我们前端来说就是所谓的基础知识,咋一看好像用处并没有很大,但掌握好这个知识点,就可以让我们在以后可以走的更远,走的更快!目录数据结构:栈调用栈是什么?用来做什么?调用栈的运行机制调用栈优化内存调用栈debug大法数据结构:栈栈是一种遵从后进先出(LIFO)原则的有序集合,新元素都靠近栈顶,旧元素都接近栈底。生活中的栗子,帮助一下理解:

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


目录

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

数据结构:栈

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

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

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

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

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

调用栈的运行机制

机制

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

看一下例子帮助理解:

// 调用栈中的执行步骤用数字表示
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的步骤被省略了):

image.png

调用侦

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

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

找到一张图片,调用侦:

image.png

调用栈优化内存

调用栈的内存消耗

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

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

针对这种情况除了我们要尽量避免函数层级嵌套的比较深之外,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)。

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

注意

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

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

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

更多

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

调用栈debug大法

查看调用栈有什么用

  1. 查看函数的调用顺序是否跟预期一致,比如不同判断调用不同函数。
  2. 快速定位问题/修改三方库的代码。
    当接手一个历史项目,或者引用第三方库出现问题的时候,可以先查看对应API的调用栈,找到其中涉及的关键函数,针对性的修复它。
    通过查看调用栈的形式,帮助我快速定位问题,修改三方库的源码。

如何查看调用栈

  1. 只查看调用栈:console.trace
a()
function a() {
    b();
}
function b() {
    c()
}
function c() {
    let aa = 1;
    console.trace()
}

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

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

image.png

积跬步以至千里

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

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

结语

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

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



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


目录
相关文章
|
2月前
|
存储 JavaScript 前端开发
深入理解JavaScript中的事件循环(Event Loop):机制与实现
【10月更文挑战第12天】深入理解JavaScript中的事件循环(Event Loop):机制与实现
124 3
|
1月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
122 62
|
1月前
|
JavaScript 前端开发
如何使用时间切片来优化JavaScript动画的性能?
如何使用时间切片来优化JavaScript动画的性能?
|
28天前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
69 31
|
1月前
|
JavaScript 前端开发 安全
ECMAScript 6(以下简称 ES6)的出现为 JavaScript 带来了许多新的特性和改进,其中 let 和 const 是两个非常重要的关键字。
ES6 引入了 `let` 和 `const` 关键字,为 JavaScript 的变量管理带来了革新。`let` 提供了块级作用域和暂存死区特性,避免变量污染,增强代码可读性和安全性;`const` 用于声明不可重新赋值的常量,但允许对象和数组的内部修改。两者在循环、函数内部及复杂项目中广泛应用,有助于实现不可变数据结构,提升代码质量。
29 5
|
1月前
|
缓存 前端开发 JavaScript
JavaScript加载优化
JavaScript加载优化
|
1月前
|
缓存 前端开发 JavaScript
优化CSS和JavaScript加载
优化CSS和JavaScript加载
|
1月前
|
缓存 JavaScript 前端开发
介绍一下 JavaScript 中数组方法的常见优化技巧
通过合理运用这些优化技巧,可以提高 JavaScript 中数组方法的执行效率,提升代码的整体性能。在实际开发中,需要根据具体的业务场景和数据特点选择合适的优化方法。
33 6
|
1月前
|
缓存 前端开发 JavaScript
优化CSS和JavaScript加载
Next.js和Nuxt.js在优化CSS和JavaScript加载方面提供了多种策略和工具。Next.js通过代码拆分、图片优化和特定的CSS/JavaScript优化措施提升性能;Nuxt.js则通过代码分割、懒加载、预渲染静态页面、Webpack配置和服务端缓存来实现优化。两者均能有效提高应用性能。
|
1月前
|
JavaScript 安全 中间件
深入浅出Node.js中间件机制
【10月更文挑战第36天】在探索Node.js的奥秘之旅中,中间件的概念如同魔法一般,它让复杂的请求处理变得优雅而高效。本文将带你领略这一机制的魅力,从概念到实践,一步步揭示如何利用中间件简化和增强你的应用。