上个月写过一篇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的运行,结合执行上下文
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。