当一段代码被执行时,JavaScript 引擎先会对其进行编译,并创建执行上下文
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
那为什么会出现这种错误呢?这就涉及到了调用栈的内容。你应该知道 JavaScript 中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。因此要讲清楚调用栈,你还要先弄明白函数调用和栈结构。
var a = 2
function add(){
var b = 10
return a+b
}
add()
函数执行之前JavaScript 引擎会为这段代码创建全局执行上下文,包含了声明的函数和变量
执行上下文准备好以后,开始执行代码,当执行到add 这时, JavaScript 判断这是一个函数调用,将执行一下操作:
- 首先,从全局执行上下文中,取出add函数代码
- 其次,对add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码
- 最后,执行代码,输出结果
这样的话函数执行的时候就存在两个执行上下文——全局执行上下文和add函数的执行上下文。
问题1: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)
调用栈变化:
第一步,创建全局执行上下文,将其压入栈底,变量a、函数add和函数addAll 都保存到了全局执行上下问的变量环境对象中。
全局执行上下入栈后,JavaScript引擎开始执行全局代码, 首先执行a=2 赋值操作,全局上下文变量环境中的a值被设置为2
第二步,调用addAll函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中。
addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。
第三步,执行 add 函数,同样会为其创建执行上下文,并将其压入调用栈
当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9
紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。
整个JavaScript 流程执行结束,
总结:调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。
栈溢出(Stack Overflow
现在你知道了调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。不过还有一点你要注意,调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。
这是因为当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。