4

这篇文章内容参考极客时间的浏览器课程(付费)

前言

我们在写代码的过程中,或多或少或者不经意间都会遇到栈溢出的问题,如下:
1.png
为什么会出现这个问题呢?要弄清楚原因,需要先弄清楚调用栈。

调用栈

函数是js中的最高公民,日常编码中函数调用函数是屡见不鲜呐。调用栈就是用来管理函数调用关系的一种数据结构。

函数调用

函数调用太简单,看代码分析

var a = 2;
function add() {
    var b = 10;
    return a + b;
}
add();

分析这段代码的执行过程

  • 编译阶段

我们在上一篇文章中已经介绍过js的执行流程,编译阶段这里就不做详细讲解了,编译结束后会生成:

  1. 全局执行上下文
  2. 可执行代码

如下图:

2.png

  • 执行阶段

生成可执行代码之后,JS引擎开始顺序执行代码,执行到add这里时,JS引擎判断出这里是函数调用,然后执行下面操作:

  1. 从全局上下文中,取出add函数代码
  2. 对add函数的这段代码进行编译(创建该函数的执行上下文环境和可执行代码)
  3. 执行add函数,输出结果

3.png
函数调用完毕,在执行add函数时,会存在两个执行上下文,一个是全局执行上下文,一个是add函数的执行上下文。

那么JS引擎是怎么管理多个执行上下文的呢,JS引擎是通过栈来管理这些执行上下文的。

栈其实很简单啦,本来决定略过,考虑到部分初学者和非科班的朋友,决定还是简单描述一下。

假如现在有一个只能放一本书的盒子和一堆书,把书放入盒子之后再拿出来,那就只能从上往下拿出来了,后面放进去的先拿出来。

栈就是这个盒子,最大的特点——先进后出

调用栈

调用栈就是管理这些执行上下文的栈,就叫调用栈。每次创建好一个执行上下文之后,就会放入调用栈中。

看下边这个例子

var a = 2;
function add(b, c) {
    return b + c;
}

function addAll(b, c) {
    var d = 10;
    var result = add(b, c);
    return a + result + d;
}

addAll(3, 6);

在上面这段代码中,在addAll函数中调用了add函数,现在我们来逐步分析调用栈是如何变化的

  • 第一步,创建全局执行上下文,并将其压入栈底,如下图:

4.png

从图中可以看出,变量a、函数add、函数addAll都保存到全局执行上下文的变量环境对象中。

全局执行上下文环境压入调用栈后,JS引擎开始执行全局代码。

a = 2;

该语句会将全局执行上下文变量环境中a的值设置为2。全局执行上下文环境状态如下图:
5.png

addAll(3, 6);

当调用addAll函数时,JS引擎会编译addAll函数,并为addAll创建一个执行上下文,最后将addAll函数的执行上下文环境压入栈中,如下图:
6.png
addAll函数的执行上下文创建成功之后,接着执行addAll函数的可执行代码。

d = 10;
result = add(b, c);
return a + result + d;

执行到add函数调用语句时,同样会为add函数创建一个执行上下文环境,并将其压入调用栈,如下图所示:
7.png
创建好add函数的执行上下文环境之后,接着执行add函数的可执行代码

return b + c;

add函数返回时,add函数的执行上下文环境就会从调用栈顶部弹出,并将result的值设置为add函数的返回值,也就是9,如下图:
8.png
然后执行addAll函数中的接下来可执行代码

return a + result + d;

这个语句执行完成之后,把结果返回,addAll函数的执行上下文环境也会从调用栈顶部弹出,此时调用栈中就只剩下全局执行上下文了。如下图所示:
9.png
至此,整个JS流程执行结束。

调用栈是JS引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各个函数之间的调用关系。

开发中,如何利用调用栈
  • 利用浏览器查看调用栈的信息

打开开发者工具(f12) -> source -> 打断点 -> 刷新
就可以通过右边的“call stack”来查看当前的调用栈的情况。如下图:

10.png

从图中可以看出,右边的"call stack"下面显示出来了函数的调用关系:
栈的底部是anonymous,也就是全局的函数入口;中间是addAll函数;顶部是add函数。非常清晰的反应了函数的调用关系。所以在分析复杂的代码时,调用栈是非常有用的。

  • console.trace()

也可以在代码中添加console.trace()来输出函数的调用关系,如在add函数中增加console.trace(),如下图:
11.png

  • 栈溢出

调用栈是用来管理执行上下文的数据结构,先进后出。需要注意的是,它是有大小的,当入栈的执行上下文超过了一定数目,JS引擎就会报错,这种错误就叫做栈溢出。

递归函数,很容易出现栈溢出,如:

function add(a, b) {
    return add(a, b);
}
add(1,2);

当执行时,就会出现栈溢出情况。

分析:
当JS引擎开始执行add函数时,就会为add函数创建执行上下文环境并压入调用栈中,但是,这个函数是递归的并且没有终止条件,所以JS引擎会一直创建新的函数执行上下文,并反复将其压入调用栈中,当超过调用栈的最大限度之后,就会出现栈溢出错误。

理解调用栈、栈溢出之后,平时写代码就可以更好的避免栈溢出的情况出现。

写在最后

这几天有点偷懒啦,坚持输出技术文章,对于我这种手残党,确实是有点难坚持。后续考虑转载好的技术文章,先保证每日一更吧。

欢迎关注我的公众号:匿名程序媛
一起挖坑、填坑
image

技术交流qq群:936183824


Rhinoceros
180 声望12 粉丝

以终为始