上个月写过一篇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>
执行的结果如下
也就是说我测试电脑当时递归了11421次左右之后栈溢出了,11421这个数字根据电脑配置不同可能有一些出入。Maximum call stack size exceeded超出最大调用栈的大小了。
问题已经有了,为什么会报错呢?带着这个疑问我们继续往下看。
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>
执行代码后的截图
第一种方式通过截图可以在第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的运行,结合执行上下文