首发:https://www.love85g.com/?p=1723javascript
在这篇文章中,我将深刻研究JavaScript最基本的部分之一,即执行上下文。在这篇文章的最后,您应该更清楚地了解解释器要作什么,为何在声明一些函数/变量以前可使用它们,以及它们的值是如何肯定的。java
什么是执行上下文?小程序
当代码在JavaScript中运行时,执行它的环境是很是重要的,并被评估为如下之一:浏览器
1:全局代码——第一次执行代码的默认环境。函数
2:函数代码——每当执行流进入函数体时。this
3:要在内部Eval函数中执行的文本。spa
您能够在线阅读大量参考资料,其中涉及scope ,本文的目的是使事情更容易理解,让咱们将术语 execution context(执行上下文) 看做当前代码正在计算的环境/范围。如今,讨论得够多了,让咱们来看一个包含 global 和 function / local 上下文计算代码的示例。线程
这里没什么特别的,咱们有1个 global context 用紫色边框表示,3个不一样的function contexts 用绿色、蓝色和橙色边框表示。只能有一个 global context ,它能够从程序中的任何其余上下文访问。指针
您能够有任意数量的 function contexts ,而且每一个函数调用都建立一个新的上下文,该上下文建立一个私有范围,其中函数内部声明的任何内容都不能从当前函数范围外部直接访问。在上面的例子中,一个函数能够访问当前上下文以外声明的变量,可是外部上下文不能访问其中声明的变量/函数。为何会这样?这段代码到底是如何计算的?code
执行上下文堆栈
浏览器中的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/...
欢迎关注小程序,感谢您的支持!