在这篇文章中,我将深入探讨JavaScript的一个最基本的部分,即执行上下文。在本文结束时,您应该更清楚地了解js解释器尝试做什么,为什么某些函数/变量可以在他们声明前使用,以及它们的值是如何确定的。

什么是执行上下文

当js代码运行时,它的执行环境非常重要,并且为以下之一:

  1. 全局代码:首次执行代码的默认环境。
  2. 函数代码 - 每当执行流程进入函数体时。
  3. Eval代码 - 要在内部eval函数内执行的文本。

你可能在网上读了很多文章关于作用域,这篇文章的目的是让你更容易的理解他们。让我们将执行上下文视为当前正在运行代码的环境/作用域。现在我们来看一个简单的例子,包含全局和函数(局部)环境下运行代码。

这里没有什么特别之处,我们有一个由紫色边框表示的全局上下文和由绿色,蓝色和橙色边框表示的3个不同的函数上下文。只能有1个全局上下文,并且它可以被程序中的任何其他上下文访问。

您可以拥有任意数量的函数上下文,并且每个函数调用都会创建一个新的上下文,从而创建一个私有作用域,在函数内部声明的任何内容都无法从当前函数作用域外直接访问。在上面的示例中,函数可以访问在其当前上下文之外声明的变量(全局上下文中的变量),但外部上下文无法访问person函数中的变量/函数。为什么会这样?这段代码究竟是如何评估的?

执行上下文栈

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

我们已经知道,当浏览器首次加载脚本时,它默认进入全局执行上下文。如果在全局代码中调用一个函数,程序的顺序流进入被调用的函数,创建一个新的执行上下文并将该上下文推送到执行堆栈的顶部。

如果在当前函数中调用另一个函数,则会发生同样的事情。代码的执行流程进入内部函数,该函数创建一个新的执行上下文,该上下文被推送到现有堆栈的顶部。浏览器将始终执行位于堆栈顶部的当前执行上下文,并且一旦函数完成执行当前执行上下文,它将从堆栈顶部弹出,将控制权返回到当前堆栈中的下一个z上下文。下面的示例显示了递归函数和程序的执行堆栈:

代码只调用自身3次,将i的值递增1.每次调用函数foo时,都会创建一个新的执行上下文。一旦上下文完成执行,它就会弹出堆栈并且控制返回到它下面的上下文,直到再次达到全局上下文。

有五个关键点可以帮助你记忆执行上下文

  1. 单线程
  2. 同步执行代码
  3. 一个全局环境
  4. 可以有很多个函数环境
  5. 每个函数调用都会创建一个新的执行上下文,对自身的调用也是

执行上下文的细节

现在我们知道每次调用一个函数时,都会创建一个新的执行上下文。但是,在JavaScript解释器中,对执行上下文的每次调用都有两个阶段:

  1. 创建(准备)阶段(函数被调用,但是函数内部代码还未执行)

    • 创建范围链
    • 创建变量,函数和函数参数
    • 确定“this”的值
  2. 激活/代码执行阶段( 分配值,引用函数,解释/执行代码

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

1
2
3
4
5
6
7
8
9
executionContextObj = {
'scopeChain': {},
'variableObject': {
// 函数的参数/传入的参数
// 内部函数
// 内部变量
},
'this': {}
}

变量对象[VO] / 激活对象[AO]

executionContextObj在函数调用时创建,但是在函数代码被执行之前。这就是我们之前说的第1阶段,即准备阶段。在这阶段,解释器通过扫描传入的参数或函数的参数,本地函数声明,局部变量声明来创建executionContextObj。此扫描的结果将成为executionContextObj中的variableObject。

以下是解释器如何评估代码的伪概述:

  1. 发现某些代码调用了一个函数
  2. 在执行这个函数的代码前,创建了这个函数的执行上下文(execution context)
  3. 进入执行的第一阶段,准备阶段
    • 初始化作用域链(scope chain)
    • 创建名为arguments的对象。检查调用函数的参数,初始化参数名字和值(个人理解为把调用函数的实参复制一份放到VO中,都说js函数是值传递大概和这里关系很密切)
    • 扫描函数内容寻找函数定义
      • 对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名称,该属性具有指向内存中函数的引用指针
      • 如果函数名已存在,则将覆盖引用指针值
    • 扫描函数寻找变量定义
      • 对于找到的每个变量声明,在变量对象中创建一个属性,该属性是变量名称,并将值初始化为undefined
      • 如果变量名称已存在于变量对象中,则不执行任何操作并继续扫描
    • 确定上下文中“this”的值
  4. 激活对象 / 代码执行阶段
    执行函数代码,并在代码逐行执行时分配变量值

举个例子:

1
2
3
4
5
6
7
8
9
10
function foo(i) {
var a = 'hello'
var b = function privateB() {

};
function c() {

}
}
foo(22)

当执行foo(22)调用函数时,该函数的执行上下文在准备阶段应该是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}

如你所见,创建阶段处理定义属性的名称,而不是为它们赋值,但正式参数(实参)除外。创建阶段完成后,执行流程进入函数内,激活/代码执行阶段在函数执行完毕后如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}

关于变量提升

在掌握了关于js解释器如何创建激活对象的知识后,我们很容易解释变量,函数提升是怎么一回事。使用以下代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(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函数已经出现在VO中,所以可以访问。
​ ​
Foo被声明两次,为什么foo显示为函数而不是未定义或字符串
​接上一问,在准备阶段进行完函数扫描开始进行变量扫描,发现foo变量,但是此时VO内已经有同名的变量名,按照规则将略过。准备阶段完成,开始逐行执行代码并赋值,由于foo定义在console语句下面,所以还是输出VO中之前定义的foo,即函数。

为什么bar的值是undefined
接上一问,在准备阶段进行变量扫描时发现bar,VO中没有所以bar作为属性名,值为undefined。进入执行阶段bar赋值语句在console语句下面,所以还是输出undefined。

原文链接

传送:http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/