JavaScript基础系列第三篇:调用栈在JavaScript引擎中扮演了一个什么样的角色

上个月写过一篇V8是如何运行JavaScript(let a = 1)代码的?,写完之后我就发现,我对平常使用的工具V8引擎,偏底层的知识了解的竟然是如此甚少。同时我真正从事前端的时间还算是比较短的,那么基础也算是非常的薄弱。结合以上,我打算有时间就去从底层的角度去学习了解,便于在使用过程中的理解和解决遇到的问题,理解JavaScript的本质,能够更好的学习JavaScript。如果你跟我有同样的困惑,那我们可以结伴同行,共同学习。

本系列我会从我的视角不断的去总结:

前言

通过本篇你可以学习到以下内容:

1、为什么会存在栈溢出?
2、调用栈的定义
3、执行上下文的管理方式
4、调用栈的作用

1、内存溢出

先来看一段简单的递归调用代码吧

<script>
  function recursion(x) {
    console.log(x)
    recursion(x)
  }

  recursion(1)
</script>

执行的结果如下

image.png

也就是说我测试电脑当时递归了11421次左右之后栈溢出了,11421这个数字根据电脑配置不同可能有一些出入。Maximum call stack size exceeded超出最大调用栈的大小了。

扫描二维码关注公众号,回复: 14365596 查看本文章

问题已经有了,为什么会报错呢?带着这个疑问我们继续往下看。

2、查看调用栈的两种方式

我们再来看一段代码

<script>
  var a = 10

  function add_d() {
    var d = 40
    console.trace('add_d正在执行')
    return a + d
  }

  function add_c() {
    var c = 30
    var dd = add_d()
    console.trace('add_d已经执行结束,从call stack弹出')
    return c + dd
  }

  function add_b() {
    var b = 20
    let cc = add_c()
    console.trace('add_c已经执行结束,从call stack弹出')
    return b + cc
  }

  add_b()
  console.trace('add_b已经执行结束,从call stack弹出')
</script>

执行代码后的截图

微信截图_20220620153034.png

第一种方式通过截图可以在第5行(截图中代码的行位置)的位置打断点,在右侧就可以查看到当前的调用堆栈信息。

第二种方式通过console.trace(),我上面的代码其实已经加入了打印日志,可以直接查看

  console.trace
  add_d @ js执行过程.html:16
  add_c @ js执行过程.html:22
  add_b @ js执行过程.html:27
(匿名) @ js执行过程.html:30   // 这里的匿名相当于全局进行
js执行过程.html:16 add_d正在执行
add_d @ js执行过程.html:16
add_c @ js执行过程.html:22
add_b @ js执行过程.html:29
(匿名) @ js执行过程.html:34

js执行过程.html:23 add_d已经执行结束,从call stack弹出
add_c @ js执行过程.html:23
add_b @ js执行过程.html:29
(匿名) @ js执行过程.html:34

js执行过程.html:30 add_c已经执行结束,从call stack弹出
add_b @ js执行过程.html:30
(匿名) @ js执行过程.html:34

js执行过程.html:35 add_b已经执行结束,从call stack弹出
(匿名) @ js执行过程.html:35

通过打印日志,我们可以更清晰的发现,当当前函数执行完毕以后,它会自动的从打印日志中移除了。

同样的你可以调整console.trace()的顺序去查看到压入call stack的顺序是什么样的。这里根据我目前的经验简单总结如下:

  • 当JavaScript调用一个函数的时候,JavaScript引擎遍会为其创建执行上下文,并把该执行上下文压入调用栈,然后JavaScript引擎开始执行函数的代码。

  • 执行函数时如果又发现有函数被调用,则会继续将该函数的执行上下文压入调用栈,然后继续开始执行函数中的代码。

  • 以此类推......

  • 当某个函数执行完毕的时候,JavaScript引擎会将函数的执行上下文弹出栈。

  • 调用栈的空间满了以后,就会引发堆栈溢出的问题。

因为调用栈的空间是有限的,所以我们开篇里的小例子不断的递归,根本停不下来,迟早会发生栈溢出,也就是我上面截图的错误。

3、调用栈的定义

我们先来理解一下栈的数据结构,通过一个小故事来进行简单的理解:

自助餐厅有一堆餐盘,工作人员洗好之后,将一批餐盘一个一个的叠加到一起,由于各种原因(餐盘的摆放位置、以及方便客人拿取等等),每批餐盘都有一定的高度限制,肯定不能无限高。接下来我们就来分析其中一批被叠拼摆好的餐盘吧,先假设一下餐盘的高度是35(总共摆放了35个餐盘)

  • 每次将餐盘叠拼上去的时候就相当于入栈

  • 这个操作一直执行了35次,因为一直没有使用

  • 接着到中午开始排队吃饭的时候,到了取餐盘的地方,就有人开始从顶部取走一个餐盘(一般都是从顶部取,特殊情况这里我们就不讨论了)

  • 每次取餐盘的操作就相当于出栈

  • 这个操作一直执行了35次,因为排队吃饭就要使用餐盘,就要使用

  • 最终35个餐盘都被取走了

可以发现,先被叠拼的餐盘,要最后才被取出,而刚被叠拼的餐盘,第一个就被拿走了。遵循的原则便是:先进后出、后进先出的原则。

调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,这个时候JavaScript引擎其实是会为当前函数创建函数执行上下文,它就将该函数执行上下文放到栈顶,当从这个函数返回的时候,就会将这个函数执行上下文从栈顶弹出,这就是调用栈做的事情。也就是说执行上下文是通过调用栈来管理的。

总结

  • 调用栈的存储空间是有限的,如果一直压入栈,压入栈,就会出现内存溢出的错误。

  • 执行上下文就是存储在调用栈中的

  • 调用栈的角度了解JavaScript的运行,结合执行上下文

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7117839908055547917