什么是JavaScript执行上下文和调用栈

·  阅读 128
什么是JavaScript执行上下文和调用栈

「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战

作为一名JavaScript开发者,我们有必要比了解JavaScript语言的内部执行机制,执行上下文和调用栈是JavaScript中的关键概念之一,理解了执行上下文和调用栈更加有助于让我们了解JavaScript中的变量提升、作用域、闭包等其他概念。

前面的两篇关于作用域的文章:

函数作用域和全局作用域

什么是JavaScript作用域、作用域链?

块级作用域和暂时性死区

ES6的块级作用域和暂时性死区是什么?

执行上下文和调用栈

不要被高大上的名词所吓倒,其实执行上下文就是我们当前代码的执行环境或者作用域。

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()
复制代码

在浏览器中执行,将会得到错误提示:

image.png

在Chrome的浏览器执行上面代码进行断点调试

image.png

不管通过什么方式,我们都能看清JavaScript引擎的错误堆栈信息,并由此看清函数调用关系。

在函数执行完毕并出栈时,函数内的局部变量在下一个垃圾回收(GC)节点会被回收,该函数的执行上下文会被销毁,这也正是我们在外部无法访问函数内部定义变量的原因。

欢迎我的公众号【小帅的编程笔记】,让自己和他人都能有所收获!

分类:
前端
收藏成功!
已添加到「」, 点击更改