首发:https://www.love85g.com/?p=1723
在这篇文章中,我将深入研究 JavaScript 最基本的部分之一,即执行上下文。在这篇文章的最后,您应该更清楚地了解解释器要做什么,为什么在声明一些函数 / 变量之前可以使用它们,以及它们的值是如何确定的。
什么是执行上下文?
当代码在 JavaScript 中运行时,执行它的环境是非常重要的,并被评估为以下之一:
1:全局代码——第一次执行代码的默认环境。
2:函数代码——每当执行流进入函数体时。
3:要在内部 Eval 函数中执行的文本。
您可以在线阅读大量参考资料,其中涉及 scope,本文的目的是使事情更容易理解,让我们将术语 execution context(执行上下文)看作当前代码正在计算的环境 / 范围。现在,讨论得够多了,让我们来看一个包含 global 和 function / local 上下文计算代码的示例。
这里没什么特别的,我们有 1 个 global context 用紫色边框表示,3 个不同的 function contexts 用绿色、蓝色和橙色边框表示。只能有一个 global context,它可以从程序中的任何其他上下文访问。
您可以有任意数量的 function contexts,并且每个函数调用都创建一个新的上下文,该上下文创建一个私有范围,其中函数内部声明的任何内容都不能从当前函数范围外部直接访问。在上面的例子中,一个函数可以访问当前上下文之外声明的变量,但是外部上下文不能访问其中声明的变量 / 函数。为什么会这样? 这段代码究竟是如何计算的?
执行上下文堆栈
浏览器中的 JavaScript 解释器是作为一个线程实现的。这实际上意味着,在浏览器中,一次只能发生一件事,其他操作或事件将排队在所谓的执行堆栈中。下图是单线程栈的抽象视图:
我们已经知道,当浏览器第一次加载脚本时,默认情况下它会进入 global execution context。如果在全局代码中调用一个函数,程序的序列流将进入被调用的函数,创建一个新的 execution context 并将该上下文推到 execution stack 的顶部。
如果在当前函数中调用另一个函数,也会发生同样的事情。代码的执行流进入内部函数,该函数创建一个新的 execution context,并将其推到现有堆栈的顶部。浏览器将始终执行位于堆栈顶部的当前 execution context,一旦函数执行完当前
execution context,它将从堆栈顶部弹出,将控制权返回到当前堆栈中下面的上下文。下面的例子展示了一个递归函数和程序的 execution stack :
(function foo(i) {if (i === 3) {return;}
else {foo(++i);
}
}(0));
代码简单地调用自身 3 次,将 i 的值增加 1。每次调用函数 foo 时,都会创建一个新的执行上下文。一旦上下文执行完毕,它就会从堆栈中弹出并返回到它下面的上下文,直到再次到达 global context 为止。
关于执行堆栈,有 5 个关键点需要记住:
1:单线程的。
2:同步执行。
3:1 个全局上下文。
4:无限的函数上下文。
5:每个函数调用都会创建一个新的执行上下文,甚至是对自身的调用。
详细执行上下文
现在我们知道,每次调用一个函数,都会创建一个新的 execution context。然而,在 JavaScript 解释器中,对 execution context 的每个调用都有两个阶段:
1:创建阶段 [当函数被调用,但在执行任何代码之前]:
创建范围链。
创建变量、函数和参数。
确定“this”的值。
2:激活 / 代码执行阶段:
为函数赋值、引用并解释 / 执行代码。
可以将每个 execution context(执行上下文)概念上表示为一个具有 3 个属性的对象:
executionContextObj = {'scopeChain': { /* 变量对象 + 所有父执行上下文的变量对象 */},
'variableObject': {/* 函数参数 / 参数,内部变量和函数声明 */},
'this': {}}
激活 / 变量对象 [AO/VO]
这个 executionContextObj 在调用函数时创建,但在实际函数执行之前创建。这被称为阶段 1,创建阶段。在这里,解释器通过扫描函数寻找传入的参数或参数、局部函数声明和局部变量声明来创建 executionContextObj。该扫描的结果成为 executionContextObj 中的 variableObject。
下面是解释器如何评估代码的伪概述:
找到一些代码来调用函数。
在执行函数代码之前,创建执行上下文。
进入创作阶段:
初始化范围链。
创建变量对象:
创建 arguments 对象,检查参数上下文,初始化名称和值,并创建引用副本。
扫描上下文中的函数声明:
对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针。
如果函数名已经存在,则重写引用指针值。
扫描上下文变量声明:
对于找到的每个变量声明,在变量对象中创建一个属性,即变量名,并初始化值为 undefined。
如果变量名已经存在于变量对象中,则什么也不做,继续扫描。
确定上下文中“this”的值。
激活 / 代码执行阶段:
在上下文中运行 / 解释函数代码,并在逐行执行代码时分配变量值。
让我们来看一个例子:
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 定义术语提升的资源,解释变量和函数声明被提升到函数作用域的顶部。但是,没有人详细解释为什么会发生这种情况,而且有了解释器如何创建 activation object(激活对象) 的新知识,就很容易理解为什么会发生这种情况。以下面的代码为例:
(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 没有定义?
bar 实际上是一个具有函数赋值的变量,我们知道这些变量是在创建阶段创建的,但是它们是用 undefined 值初始化的。
总结
希望现在您已经很好地理解了 JavaScript 解释器是如何评估代码的。理解执行上下文和堆栈可以让您了解代码为什么要计算您最初没有预料到的不同值的原因。
您是否认为了解解释器的内部工作方式对您的 JavaScript 知识来说是太大的开销还是必需的? 了解执行上下文阶段是否有助于编写更好的 JavaScript ?
原文:http://davidshariff.com/blog/…
欢迎关注小程序,感谢您的支持!