JavaScript中的执行上下文和堆栈是什么

983 阅读5分钟

在这篇文章中,将深入研究JavaScript最基本的部分之一,即执行上下文。在这篇文章的最后,你应该更清楚地理解解释器要做什么,为什么在声明一些函数/变量之前可以使用它们,以及它们的值是如何确定的。

什么是执行上下文

当JavaScript代码运行时,执行代码的环境是相当重要的。一般有以下三种情况:

  • 全局代码 -- 代码首次开始执行的默认环境
  • 函数代码 -- 每当进入一个函数内部
  • Eval代码 -- eval内部代码执行时

把执行上下文看作是当前代码正在执行的环境/作用域

// global context
var sayHello = 'sayHello'

function person() {
  var first = 'webb'
  var last = 'wang'

  function firstName() {
    return first
  }

  function lastName() {
    return last
  }

  console.log(sayHello + firstName() + '' + lastName())
}

以上代码没什么特别的地方,它包括1个全局上下文和3个不同的函数上下文,全局上下文可以被程序中的其它任何上下文访问。

你可以有任意数量的函数上下文,每个函数被调用的时候都会创建一个新的上下文。每个下文都有一个不能被外部函数直接访问到的内部变量的私有作用域。在上面代码的例子中,一个函数可以访问当前上下文外部声明的变量,但是一个外部上下文不可以访问函数内部声明的变量。

执行上下文堆栈

浏览器中的JavaScript解释器是作为一个单线程实现的,这实际上意味着,在浏览器中,一次只能发生一件事,其他操作或事件将排队在所谓的执行堆栈中。

当浏览器开始执行脚本时,首先会默认进入全局执行上下文,如果在全局代码中调用了函数,程序会按照顺序进入被调用函数,创建一个新的执行上下文,并推入到执行栈的栈顶。

如果你在当前执行的函数中,调用了另外的函数,代码的执行流将会进入函数内部,并创建一个新的执行上下文推入到执行栈顶。浏览器总是会先执行栈顶的代码,并且一旦函数完成执行当前执行上下文,他就会从栈顶弹出,将控制权返回到当前堆栈中的上下文。

关于执行堆栈有以下关键点

  • 单线程
  • 同步执行
  • 1个全局上下文
  • 每个函数调用都会创建一个新的执行上下文,即使调用它自身。

深入理解执行上下文

现在我们知道每当有函数被调用时,都会创建一个新的执行上下文。在js内部,每个执行上文创建都要经历下面2个阶段

1.创建阶段(函数被调用,但还没有执行内部代码)

  • 创建作用域链
  • 创建变量和参数
  • 决定this指向

2.代码执行阶段

  • 变量赋值,执行代码

可以将每个执行上下文概念上表示为一个具有3个属性的对象:

executionContextObj = {
  'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
  'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
  'this': {}
}

活动对象/变量对象(AO/VO)

当函数被调用时,在创建阶段解释器会创建包含有函数内部变量,参数的一个变量对象

下面是解释器如何评估代码的概述

  1. 扫描被调用函数中的代码
  2. 在代码执行前,创建执行上文
  3. 进入创建阶段
    • 初始化作用域链
    • 创建变量对象
    • 创建arguments对象,检查参数上下文,初始化名称和值,并创建引用副本
    • 扫描上下文中函数的声明
      • 对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针
      • 如果函数名已经存在,指针将会被覆盖
    • 扫描变量的声明
      • 对于找到的每个变量,在变量对象中创建一个属性,该属性是确切的变量名,该变量的值是undefined
      • 如果变量名已经存在,将不会做任何处理继续执行
    • 决定this的值
  4. 代码执行阶段
    • 变量赋值,按顺序执行代码

声明提升

你可以在网上找到许多用JavaScript定义术语提升的资源,解释变量和函数声明被提升到函数作用域的顶部。但是,没有人详细解释为什么会发生这种情况,而且有了解释器如何创建激活对象的新知识,就很容易理解为什么会发生这种情况。以下面的代码为例:

(function() {
  console.log(typeof foo); // function pointer
  console.log(typeof bar); // undefined

  var foo = 'hello',
      bar = function() {
          return 'world';
      };

  function foo() {
      return 'hello';
  }

}());​

为什么在什么之前可以访问到foo

如果我们遵循创建阶段,我们就知道在代码执行阶段之前已经创建了变量。因此,当函数流开始执行时,foo已经在活动对象中定义。

Foo声明了两次,为什么Foo是函数而不是未定义或字符串?

尽管foo声明了两次,但从创建阶段我们就知道函数是在变量之前在变量对象上创建的,如果变量对象上的属性名已经存在,那么我们只需绕过。 因此,首先在变量对象上创建对函数foo()的引用,当解释器到达var foo时,我们已经看到了属性名foo的存在,所以代码什么也不做,继续执行

为什么bar是undefined

bar实际上是一个具有函数赋值的变量,我们知道这些变量是在创建阶段创建的,但是它们是用undefined值初始化的。

总结

希望现在你已经很好地理解了JavaScript解释器是如何执行代码的。理解执行上下文和堆栈可以让你了解代码没有按照预期执行的原因