JavaScript调用栈
在我们的日常开发中,相信大家应该会遇到过下边这种报错。从字面意思我们能理解为出现了栈溢出,栈占用的空间比分配给它的空间还大,常见的原因是两个函数之间的互相调用,还有就是没有终止条件的递归。栈大家都知道是一种数据结构,那么在这里的栈又是什么呢。那么我们今天来好好了解一下调用栈,概括来说它是用来管理函数调用关系的数据结构。
首先我们来说说函数调用,它是指我们声明一个函数并使它运行,具体的写法也就是一个函数名称后边加上一对圆括号。举个板栗
1 | var name = 'able'; |
在执行函数之前,我们用上一次的学习可以得知,这段代码会生成对应的全局执行上下文,其中的变量环境里包含了声明的函数sayHello和全局变量name、age。接下来开始执行全局代码,这里的sayHello便是一个函数调用,具体会有以下几个步骤。第一步是从全局执行的上下文中拿到声明的函数代码,然后是对这段函数对代码进行编译,创建函数对应的执行上下文和可执行代码,最后执行代码并输出结果。这里我们可以得知,在执行JS代码的时候可能会有多个执行上下文的存在,比如全局执行上下文和函数执行上下文,那么这里我们就能用栈这种数据结构,来管理这些执行上下文。
说到栈我们能想到最大的特点就是后进先出了,那么在执行上下文创建好以后,它们便会被压入到栈中,我们称之为调用栈,也叫执行上下文栈。为了更好理解它,我们来写一段比开头那段复杂些的代码。
1 | var a = 6; |
he上边的情况是在一个函数中调用了另一个函数,我们来详细的分析一下过程。
首先创建全局上下文并入栈,当前生成的全局上下文的变量环境对象中有变量a、函数by和byTotal。当它入栈后,全局代码被执行,首先6被赋给a,此时调用栈中的情况如下图所示。
然后是调用byTotal函数,调用时我们首先需要对其进行编译,也是为其创建一个函数执行上下文,并使其进入栈中,如下图所示。
执行上下文创建好后我们开始执行函数。首先对d进行赋值5,然后我们会执行到by函数,也会创建一个by函数的执行上下文,并使其入账,如下图所示。
那么什么时候开始出栈呢,便是by函数的执行结果返回后,它对应的函数执行上下文便会从栈顶弹出啦,此时byAll中的result得到了返回值3,如下图所示。
然后在byAll执行完最后一行并返回值后,它对应的函数执行上下文也会出栈,此时的调用栈中重新回到了最初的亚子,只剩下一个孤零零的全局上下文了。
通过上边的步骤,我们可以了解到调用栈的作用了,它可以很直观的给我们展示某个函数在被执行,特别是函数调用较多时,能让我们很清楚各个函数之间的调用关系。
那么讲完了原理,它在实践层面上的应用我们一起来看看。在大家用chrome调试的时候,source中debug的位置有一个Call Stack不知道大家还有没有印象呢。我们在source中新建一个代码块(new snippet),把上边那段demo放入js文件中,打上断点来执行一下这段代码。
我们在by函数中打上断点,右键点击run,可以看到图中的代码执行到断点处暂停了,右侧有Call Stack这一栏可以看到当前调用栈的情况,就跟我上图中画的一致,从底部到顶部依次是anonymous,也就是全局的函数入口,往上是byAll函数,最后顶部是by函数。我们能很直观明了的得知函数之间的调用关系。可能demo比较简单啊,但是在分析较为长的复杂的代码时,它是比较有效的,包括在定位问题时也是一样。如果不习惯打断点的童鞋,我们还有一种方式是使用一个方法——console.trace(),它的效果也是一样的,我们可以在控制台看到其结果。