作为一名JavaScript开发者,我们有必要比了解JavaScript语言的内部执行机制,执行上下文和调用栈是JavaScript中的关键概念之一,理解了执行上下文和调用栈更加有助于让我们了解JavaScript中的变量提升、作用域、闭包等其他概念。
前面的两篇关于作用域的文章:
函数作用域和全局作用域
块级作用域和暂时性死区
执行上下文和调用栈
不要被高大上的名词所吓倒,其实执行上下文就是我们当前代码的执行环境或者作用域。
JavaScript代码执行的两个阶段
理解执行上下文和调用栈,我们要从代码的执行过程说起,虽然在平时的开发时并不会涉及,但对于我们深入理解JavaScript语言和代码运行机制非常重要。
JavaScript执行主要有两个阶段:
- 代码预编译
- 代码执行
预编译主要是在代码被执行之前,JavaScript引擎会做一些预先处理的工作。
代码执行阶段的主要是执行代码逻辑,执行上下文会在这个阶段全部创建完成。
在通过语法分析无误后,首先会在预编译阶段对代码中变量的内存空间进行分配,变量提升也是在此阶段完成的。
预编译过程的3件事件
- 进行变量声明
- 对变量声明进行提升为undefined
- 对所有非表达式的函数声明进行提升
记住上面3件事情有助于我们更好的正确理解代码的逻辑,下面我们通过一些面试题目来巩固这些知识。
下面代码将输出什么?
function bar() {
console.log('bar1')
}
var bar = function () {
console.log('bar2')
}
bar()
上面代码的两个bar在预编译阶段都会被提升到顶部,然而在执行阶段bar最终被赋值为函数表达式console.log('bar2')
。如果将函数的调用时机放到var bar = funciton
之前,那结果就不同了,在函数表达式未执行之前,bar()
调用的将是console.log('bar1')
。
所以将输出bar2,我们再将代码调整顺序执行
var bar = function() {
console.log('bar2')
}
function bar() {
console.log('bar1')
}
bar()
与上面代码相同,变量bar在预编译阶段仍然会被提升至顶部,然后函数bar也被创建并提升到顶部。然后变量虽然被提升,但在代码执行阶段,其才会被赋值。所以上面代码仍会输出bar2
。
这也是需要大家所熟知的地方。
下面我们通过这道题目来加深理解:
foo(10)
function foo(num) {
console.log(foo)
foo = num
console.log(foo)
var foo
}
console.log(foo)
foo = 1
console.log(foo)
执行上面代码将输出
undefined
10
function foo(num) {
console.log(foo)
foo = num
console.log(foo)
var foo
}
1
我们来一步步进行解析
- 在foo(10)执行时,函数内进行变量foo进行提升,所以第一行输出将是undefined,执行第三行将输出10
- 运行到函数外的
console.log(foo)
时,会输出foo函数,因为函数内的foo = num
,num是被赋值给函数内的局部变量foo。 - 最后foo被赋值成1,所以最后会输出1。
调用栈
调用栈也是一个非常简单的概念,我们在执行一个函数时,这个函数又调用了另一个函数,而这另一个函数又调用了另一个函数,这一系列就称之为调用栈。
比如下面的代码
function fn1() {
fn2()
}
function fn2() {
fn3()
}
function fn3() {
fn4()
}
function fn4() {
console.log('fn4')
}
fn1()
上的代码调用关系为fn1->fn2->fn3->fn4。
fn1先入栈,fn1调用了fn2,fn2再入栈,以此类推,指导fn4执行完成,fn4先出栈,fn3再出栈,然后fn2,最后fn1出栈。这个过程满足先进后出(后进先出)的原则,所以成为调用栈。
我们将fn4()函数的代码故意改错
function fn1() {
fn2()
}
function fn2() {
fn3()
}
function fn3() {
fn4()
}
function fn4() {
log('fn4')
}
fn1()
在浏览器中执行,将会得到错误提示:
在Chrome的浏览器执行上面代码进行断点调试
不管通过什么方式,我们都能看清JavaScript引擎的错误堆栈信息,并由此看清函数调用关系。
在函数执行完毕并出栈时,函数内的局部变量在下一个垃圾回收(GC)节点会被回收,该函数的执行上下文会被销毁,这也正是我们在外部无法访问函数内部定义变量的原因。
欢迎我的公众号【小帅的编程笔记】,让我们在前端的路上越走越远