前言
老规矩了,上篇博客我说到了执行上下文和可执行代码,变量提升,但我也只讲到了这些基础的部分,比如执行怎么执行?在哪里执行?整个执行流程是咋样的?
一般来说,js代码有这三种情况
- js执行全局代码的时候,编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局上下文只有1份
- 调用一个函数时,函数体内的代码又会被编译,并且创建函数执行上下文,一般情况下,函数执行完之后,这个函数的函数执行上下文会被销毁(特殊情况是闭包,造成内存泄漏)
- 当使用eval函数的时候,eval的代码也会被编译,创建执行上下文
好了,又进一步理解了执行上下文,那么就在这个基础上继续深入了解,聊聊调用栈
学习调用栈至少有3点好处
- 帮助理解js引擎背后的工作原理
- 让你有调试js的调试能力
- 帮助你找工作啦,这也是出镜率很高的题目
我相信大家在初学js的时候肯定遇到过这个问题
可能你当时不知道为什么会出现这个问题,因为这涉及到调用栈的内容,而且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,那么整个执行过程是怎样的呢?
- 首先遇到的是全局代码,创建全局执行上下文,并将其压入执行栈的栈底,同时全局执行上下文中的变量环境有变量a,add函数和addAll函数
- 全局上下文入栈后,开始执行可执行代码,首先赋值a=2,然后遇到addAll(),这时候开始调用addAll函数
- js引擎先编译addAll函数,为其创建一个函数执行上下文,函数执行上下文创建好之后,将addAll压入执行栈 ,执行函数里的可执行代码
- 首先赋值d=10,然后遇到了result = add(b,c)代码,这时候又要调用add函数了,又创建add的函数执行上下文,然后压入栈,开始执行add函数里的代码
- add函数里的代码执行完之后,返回一个结果,这时候add函数的执行上下文出栈(pop)
,返回一个结果给addAll里的result, - addAll函数将a+result+d执行完之后,返回一个结果,出栈
- 此时栈中只剩下了全局执行上下文了,至此,整个js流程就结束了(全局上下文在页面关闭的时候出栈)
好了,至此应该理解了调用栈的一个机制了,当一次有多个函数调用的时候,就是调用栈来管理这些函数,通过调用栈就能了解各函数之间的关系
如何利用好调用栈呢?
1.利用浏览器查看调用栈信息
当代码非常复杂时,我们很难自己分析这里面的函数调用关系,这时我们就需要断点和使用浏览器的开发者工具来调试代码了具体操作如下
分析复杂的代码或者检查bug时,开发者工具都是非常好用的一个工具
除了通过断点,还可以使用console.trace()来打印当前的函数调用关系,从控制台输出结果,不过我觉得不如直接使用工具
2.栈溢出(stack Overflow)
现在应该知道了调用栈就是管理执行上下文的一种数据结构不过还有一点就是调用栈是有大小的,当入栈的执行上下文超过一定数目,就会报错,我们把这种错误叫做栈溢出
特别是递归代码的时候,如果没有退出条件就会一直自己调用自己
function division(a,b){
return division(a,b)
}
division(1,2)
js执行这个代码的时候会一直创建division这个函数的执行上下文,然后一直入栈,直到超过栈的大小,才报错
理解了这个,写代码的时候就可以避免这样的错误,比如递归改成迭代,或者使用定时器拆分方法等
好了,今天就写到这里了,关键是我也写累了…
唉,整理知识好难…
最后搞个小题目:
var a = 1
function foo() {
var a = 2
console.log(a)
}
foo()
console.log(a)
打印结果就是 2 1
foo()函数执行完后,打印完2,它的执行上下文出栈也就是说它执行上下文里面的变量环境里面的a没有了,此时全局上下文里面打印的a是全局执行上下文的变量环境里的a
好了.完事