今天是系列第六篇,主要讲一下执行上下文和调用栈的一些知识点。之前我们在第二篇文章讲到了作用域的一些知识,那篇文章提到还有一些执行上下文,变量提升之类的知识点因为篇幅的问题没有讲清楚,今天这篇文章就是来补充这些知识点的。

之前的那篇文章说到,js中的作用域是词法作用域(静态作用域),就是说我们在写代码的时候将变量和函数声明在哪里的时候就已经确定了,不会再变化了。之所以在这里再回顾一遍,是为了将作用域和后面的执行上下文区分开来。记住:词法作用域是在编写代码的时候就已经确定了,我们后面讲的执行上下文是在函数调用的时候创建的,这两个需要区分开。首先,还要从变量提升说起。

变量提升

先来个例子:

abs()  // 111
function abs(){
    console.log(111)
}
abs()  // 111
var abs = function(){
    console.log(222)
}
abs()  // 222

上述代码打印的值分别是111,111,222。上述代码的执行顺序其实是这样的:

function abs(){
    console.log(111)
}
var abs = undefined
abs()
abs()
abs = function(){
    console.log(222)
}
abs()

我们可以看到,打印值为111的函数是函数声明的方式;打印值为222的函数是函数表达式的方式。函数声明的方式提升优先级比函数表达式的提升要高,且函数表达式的提升过程可以看做是变量提升加赋值的过程。另外,需要记住的一点是:当同一个作用域下函数声明和函数表达式是同名的,函数声明的提升会在前面,并且后面的同名变量不会覆盖前面函数声明的赋值。那我们由这个例子可以得出两个结论:

  • 函数声明的提升优先级大于函数表达式和变量的提升
  • 函数声明的提升是将函数的创建、初始化和赋值都提升了;而函数表达式和变量的提升是将变量的创建初始化提升,赋值没有提升。

但事实上,函数执行的具体过程可能更复杂一些,那接下来我们引入执行上下文和执行栈的概念。执行上下文和执行栈都是在函数执行的时候产生的。

执行上下文

给它下个定义。它是JavaScript执行一段代码时的运行环境。执行上下文是在代码执行的时候创建的,它包括变量环境和词法环境。变量环境中保存的是通过变量提升的函数和变量,词法环境中保存的块作用域中的变量,怎么理解呢?举个简单的例子:

var a = 10
console.log(a)

我们都知道最后打印10,但是它的整个执行过程是这样的。

  • js引擎开始进入代码前,进入编译阶段。此时创建全局上下文和可执行代码。可执行代码是除变量提升之外的代码。编译结束后变量环境中有变量提升a,值为undefined,
  • 代码往下执行(即执行可执行代码),当执行完var a = 10语句后,变量环境中的a赋值为10,
  • 走到console.log(a)时,js引擎去变量环境中找变量a,找到了并且发现值是10,因此输出10

其实上面还有执行栈的概念。接下来我们把例子变复杂一点。来解释执行栈这是个啥东西。

执行栈

给它下个定义。它是用来管理执行上下文的一种数据结构。上面我们的例子是只有一个执行上下文的情况,分析起来比较简单。假如我们一段代码中有100个,1000个执行上下文呢?那是不是就乱了,那这个时候我们就引进了执行栈(一种数据结构)来管理这些执行上下文。

我来举个例子来类比一下执行栈和执行上下文:小明妈妈给了小明10个苹果(执行上下文),但是小明手上拿不了那么多,于是他找了一个罐子(执行栈)来装这些苹果,他把这些苹果一个个的放进罐子里(入栈),然后他就把这些苹果带走出去野炊了,到了野炊的地方,他就把之前装的苹果拿出来(出栈),拿出的规则是后面放进去的先拿出来(栈后进先出的规则),于是罐子就空了。

看了上面小明的例子,有了一些基本的理解了吧?OK,接下来用一个稍微复杂一点的例子来分析这段代码做了什么事。

var a = 10
function inner(){
    console.log(a)
}
function run(){
    var a = 1
    inner()
}
run()

有点复杂哦,咱们一步步来看啊。首先我们来看一下词法作用域的部分,词法作用域是在写代码的时候就决定的,是静态的。我们看这段代码中有全局作用域、函数inner作用域和函数run作用域。我们在第二篇文章学习过作用域链对吧,那我们肯定知道inner和run函数的作用域和上级作用域是什么吧。

接下来是代码执行阶段。

  • 进入代码之前创建执行栈和全局执行上下文,并且把全局执行上下文压入栈底,创建全局执行上下文中的变量环境和词法环境,我们这篇文章的例子先不引入块级作用域,那词法环境中就是空,所以下面暂时把词法环境这块细节忽略。此时全局执行上下文中的变量环境中有变量inner=function(){##},run=function(){##},a=undefined;代码往下执行到var a = 10时候,变量环境中的a=10;然后执行run()
  • 创建函数run的函数执行上下文,并且把该执行上下文压入栈中(在全局执行上下文上面),创建函数run的函数执行上下文的变量环境和词法环境,此时函数run执行上下文的变量环境中有a= undefined,代码往下执行,直到var a = 1时,变量环境a = 1;然后执行inner()
  • 创建函数inner的函数执行上下文,并且把该执行上下文压入栈中(在函数run的函数执行上下文上面),创建函数inner的函数执行上下文的变量环境和词法环境,此时函数run执行上下文的变量环境为空,代码往下执行console.log(a)。它就去当前作用域下找变量a,发现当前作用域下不存在a,那他又去上层作用域,即全局作用域中找变量a,找到了a=10,于是输出10.
  • 函数inner执行结束,将函数inner的执行上下文从调用栈中移除
  • 函数run执行结束,将函数run的执行上下文从调用栈中移除
  • 此时执行栈内还有全局执行上下文还在调用栈中。如果代码一直跑下去则全局执行上下文一直存在执行栈中;若退出程序则全局执行上下文和调用栈都销毁。

上面的过程像不像小明放苹果到罐子里,然后拿出来的过程?好了,今天的文章就讲这么多,我们来回顾一下今天的知识点。

总结

  • 作用域是在编写代码时候就确定的,执行上下文是在代码执行的时候确定的,搞清楚两者的区别。
  • 代码执行包括编译和执行两部分。编译阶段创建执行上下文和可执行代码;执行阶段执行的是可执行代码。
  • 执行上下文包括变量环境和词法环境。变量环境中存储变量提升的变量和函数,词法环境中存储块作用域中的变量。
  • 调用栈是用来管理执行上下文的一种数据结构。调用栈与执行上下文的关系就像罐子与苹果的关系。

摩根
11 声望2 粉丝

前端工程师