likes
comments
collection
share

JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?

作者站长头像
站长
· 阅读数 43

例子:

咱们先看段代码,你觉得下面这段代码输出的结果是什么?


showName()
console.log(myname)
var myname = '京东'
function showName() {
    console.log('函数showName被执行');
}

使用过 JavaScript 开发的程序员应该都知道,JavaScript 是按顺序执行的。若按照这个逻辑来理解的话,那么:

当执行到第 1 行的时候,由于函数 showName 还没有定义,所以执行应该会报错;

同样执行第 2 行的时候,由于变量 myname 也未定义,所以同样也会报错。

然而实际执行结果却并非如此, 如下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





一、变量提升(Hoisting)

不过在介绍变量提升之前,我们先通过下面这段代码,来看看什么是 JavaScript 中的声明和赋值。

(一)变量声明


var myname = '京东'

这段代码你可以把它看成是两行代码组成的:


var myname    //声明部分
myname = '京东'  //赋值部分



(二)函数声明


function foo(){
  console.log('foo')
}

var bar = function(){
  console.log('bar')
}

第一个函数 foo 是一个完整的函数声明,也就是说没有涉及到赋值操作;

第二个函数是先声明变量 bar,再把function() {console.log('bar')}赋值给 bar。 为了直观理解,你可以参考下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





好了,理解了声明和赋值操作,那接下来我们就可以聊聊什么是变量提升了。

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。





JavaScript 代码的执行流程从概念的字面意义上来看,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面。但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。对,你没听错,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。大致流程你可以参考下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





(三)代码中出现相同的变量或者函数怎么办?



    function showName() {
      console.log('京东A');
    }
    showName();
    function showName() {
      console.log('京东B');
    }
    showName();

我们来分析下其完整执行流程:

首先是编译阶段。遇到了第一个 showName 函数,会将该函数体存放到变量环境中。接下来是第二个 showName 函数,继续存放至变量环境中,但是变量环境中已经存在一个 showName 函数了,此时,第二个 showName 函数会将第一个 showName 函数覆盖掉。这样变量环境中就只存在第二个 showName 函数了。

接下来是执行阶段。先执行第一个 showName 函数,但由于是从变量环境中查找 showName 函数,而变量环境中只保存了第二个 showName 函数,所以最终调用的是第二个函数,打印的内容是“京东B”。第二次执行 showName 函数也是走同样的流程,所以输出的结果也是“京东B。综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。





JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?







二、 为什么JavaScript代码会出现栈溢出?



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?



那为什么会出现这种错误呢?这就涉及到了调用栈的内容。你应该知道 JavaScript 中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,你还要先弄明白函数调用和栈结构。



(一)函数调用


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

在执行到函数 add() 之前,JavaScript 引擎会为上面这段代码创建****全局执行上下文,包含了声明的函数和变量,你可以参考下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?



从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。

执行上下文准备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:

1.首先,从全局执行上下文中,取出 add 函数代码。

2.其次,对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。

3.最后,执行代码,输出结果 12。

我们就有了两个执行上下文了——全局执行上下文add 函数的执行上下文



也就是说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何管理这些执行上下文的呢?

答案是通过一种叫栈的数据结构来管理的。



(二)什么是栈?

在流量较大的场景中,就会发生反复的入栈、栈满、出栈、空栈和再次入栈,一直循环。所以,栈中的元素满足后进先出的特点。你可以参看下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?



(三)什么是 JavaScript 的调用栈?

JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。

在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。为便于你更好地理解调用栈,下面我们再来看段稍微复杂点的示例代码:

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

下面我们就一步步地分析在代码的执行过程中,调用栈的状态变化情况。



第一步:创建全局上下文,并将其压入栈底。

如下图所示:

JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?

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



全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。 首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。设置后的全局上下文的状态如下图所示:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





第二步: 调用 addAll 函数

当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?







addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。



第三步:当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈。

如下图所示:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?



当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。如下图所示:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





至此,整个 JavaScript 流程执行结束了。

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





三、在开发中,如何利用好调用栈

1. 如何利用浏览器查看调用栈的信息

当你执行一段复杂的代码时,你可能很难从代码文件中分析其调用关系,这时候你可以在你想要查看的函数中加入断点 然后当执行到该函数时,就可以查看该函数的调用栈了

打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,然后在第 3 行加上断点,并刷新页面。你可以看到执行到 add 函数时,执行流程就暂停了,这时可以通过右边“call stack”来查看当前的调用栈的情况,如下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





从图中可以看出,右边的“call stack”下面显示出来了函数的调用关系:栈的最底部是 anonymous,也就是全局的函数入口中间是 addAll 函数顶部是 add 函数。这就清晰地反映了函数的调用关系,所以在分析复杂结构代码,或者检查 Bug 时,调用栈都是非常有用的。

除了通过断点来查看调用栈,你还可以使用 console.trace() 来输出当前的函数调用关系,比如在示例代码中的 add 函数里面加上了 console.trace(),你就可以看到控制台输出的结果,如下图



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





2. 栈溢出(Stack Overflow)

现在你知道了调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。不过还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。

特别是在你写递归代码的时候,就很容易出现栈溢出的情况。比如下面这段代码:


function division(a,b){
    return division(a,b)
}
console.log(division(1,2))

当执行时,就会抛出栈溢出错误,如下图:



JavaScript执行顺序你真的清楚吗? 为什么会出现“栈溢出”?





从上图你可以看到,抛出的错误信息为:超过了最大栈调用大小(Maximum call stack size exceeded)。



那为什么会出现这个问题呢?这是因为当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

转载自:https://juejin.cn/post/7243950304876806200
评论
请登录