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

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

什么是执行上下文

当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个不一样的函数上下文,全局上下文能够被程序中的其它任何上下文访问。ui

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

执行上下文堆栈

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

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

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

关于执行堆栈有如下关键点code

  • 单线程
  • 同步执行
  • 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解释器是如何执行代码的。理解执行上下文和堆栈可让你了解代码没有按照预期执行的缘由

相关文章
相关标签/搜索