0
点赞
收藏
分享

微信扫一扫

【译】JS的执行上下文和环境栈是什么?

这篇文章中,我将深入探讨JavaScript中的一个最基本的部分,即执行上下文(或称环境)。读过本文后,你将更加清楚地了解到解释器尝试做什么,为什么在声明某些函数/变量之前,可以使用它们以及它们的值是如何确定的。

执行上下文是什么?

在运行JavaScript代码时,执行环境非常重要,并可以认为是以下其中之一:

  • 全局代码 - 默认环境,你的代码第一时间在这里执行。
  • 函数代码 - 当执行流进入函数体的时候。
  • Eval代码 - eval函数内部的文本。【eval不建议使用】

你可以在网上查到大量的关于​​scope(作用域)​​​的资料,本文的目的就是要让事情更加容易理解。我们把术语​​执行上下文​​​视为当前代码的评估环境/范围。现在,条件充足,我们看个包含​​全局和函数/本地​​上下文评估代码的示例。

【译】JS的执行上下文和环境栈是什么?_javascript

这里没什么特别的,我们有1个由紫色边框表示的​​全局上下文​​​和由绿色、蓝色和橙色边框表示的3个不同的​​函数上下文​​。只有1个全局上下文,我们可以从程序的任何其它上下文访问。

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

环境栈

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

【译】JS的执行上下文和环境栈是什么?_前端_02

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

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

(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));

【译】JS的执行上下文和环境栈是什么?_执行栈_03

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

关于执行栈有五个关键点:

  • 单线程
  • 同步执行
  • 1个全局上下文
  • 无限的函数上下文
  • 每个函数调用都会创建一个新的​​执行上下文​​,甚至是调用自身

执行上下文的细节

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

  1. 创建阶段【调用函数时,但是在执行里面的代码之前】:
  • 创建作用域链
  • 创建变量,函数和参数
  • 确定this的值
  1. 激活/代码执行阶段:
  • 分配值,引用函数和解析/执行代码

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

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

活动/变量对象【AO/VO】

调用函数时,但在执行实际函数之前,会创建此​​executionContextObj​​​。这被称为阶段1,即​​创建阶段​​​。这里,解释器通过扫描传入的参数或参数的函数、本地函数声明和局部函数声明来创建​​executionContextObj​​​。此扫描的结果将称为​​executionContextObj​​​中的​​variableObject​​。

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

  1. 找些代码来调用一个函数
  2. 在执行​​函数​​​代码之前,创建​​执行上下文​​。
  3. 进入创建阶段
  • 初始化​​作用域链​
  • 创建​​变量对象​​:
  • 创建​​arguments对象​​,检查参数的上下文,初始化名称和值并创建引用的副本。
  • 扫描上下文以获取函数声明:
  • 对于找到的每个函数,在​​变量对象(或活动对象)​​中创建一个属性,该属性是确切的函数名称,该函数具有指向内存中函数的引用指针。
  • 如果函数名已存在,则将覆盖引用指针值。
  • 扫面上下文以获取变量声明:
  • 对于找到的每个变量声明,在​​变量对象(或活动对象)​​中创建一个属性,该属性是变量名称,并将值初始化为undefined。
  • 如果变量名称已存在于​​变量对象(或活动对象)​​中,则不执行任何操作并继续扫描(即跳过)。
  • 确定上下文中的this
  1. 激活/代码执行阶段:
  • 在上下文中运行/解释功能代码,并在代码逐行执行时分配变量值。

看下下面的例子:

function foo(i) {
var a = 'hello';
var b = function privateB() {

};
function c() {

}
}

foo(22);

调用​​foo(22)​​,创建阶段如下:

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ...

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

fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}

“提升”一词

你可以在网上找到很多定义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显示为函数而不是undefined​string​呢?
  • 即使​​foo​​​被声明了两次,我们从​​创建阶段​​​中就知道到达变量之前在​​活动对象​​​上已经创建了函数,并且如果​​活动对象​​上已经存在属性名称,我们就会绕过了声明。
  • 因此,首先在活动对象上创建函数​​foo()​​​的引用,并且当解释器到达​​var foo​​​时,我们已经看到名称​​foo​​存在,因此代码什么都不做并且继续。
  • 为什么bar是undefined
  • ​bar​​​实际上是一个具有函数赋值的变量,我们知道变量是在​​创建阶段​​​创建的,但它们是使用​​undefined​​值初始化的。

总结

希望到现在,你已经很好地掌握了JavaScript解释器是如何评估你的代码。理解执行上下文和环境栈可以让你了解代码的评估和你预期不同值的原因。

你是认为了解解释器的内部工作原理是多余的还是必要的JavaScript知识点呢?知道执行上下文是否有助你编写出更好的JavaScript?

笔记:有些人一直在询问闭包,回调,timeout等知识点,我将在下一篇​文章中介绍,更多地关注与​​执行环境​​​相关的作用域链。

扩展阅读

  • ECMA-262 5th Edition
  • ECMA-262-3 in detail. Chapter 2. Variable object
  • Identifier Resolution, Execution Contexts and scope chains

原文: http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/

更多内容:https://github.com/reng99/blogs

举报

相关推荐

0 条评论