这篇文章中,我将深刻探讨JavaScript中的一个最基本的部分,即执行上下文(或称环境)。读过本文后,你将更加清楚地了解到解释器尝试作什么,为何在声明某些函数/变量以前,可使用它们以及它们的值是如何肯定的。javascript
在运行JavaScript代码时,执行环境很是重要,并能够认为是如下其中之一:java
你能够在网上查到大量的关于scope(做用域)
的资料,本文的目的就是要让事情更加容易理解。咱们把术语执行上下文
视为当前代码的评估环境/范围。如今,条件充足,咱们看个包含全局和函数/本地
上下文评估代码的示例。git
这里没什么特别的,咱们有1个由紫色边框表示的全局上下文
和由绿色、蓝色和橙色边框表示的3个不一样的函数上下文
。只有1个全局上下文,咱们能够从程序的任何其它上下文访问。github
你能够拥有任意数量的函数上下文
,而且每一个函数调用都会建立一个新的上下文,从而建立一个私有的做用域,没法从当前函数做用域外直接访问函数内部声明的任何内容。在上面的例子中,函数能够访问在其当前上下文以外声明的变量,可是外部上下文没法访问(函数)其中声明的变量/函数。为何会这样?这段代码到底是如何评估的?浏览器
浏览器中的JavaScript解释器是单线程实现的。这意味着在浏览器中一次只能发生一件事情,其它动做或事件在所谓的执行栈
中排队。下图是单线程栈的抽象视图:闭包
咱们知道,当浏览器首次加载脚本时,它默认进入全局执行上下文
。若是在全局代码中调用一个函数,程序的顺序流就进入被调用的函数,建立一个新的执行上下文
并将该上下文推送到执行栈
的顶部。ecmascript
若是你在当前函数中调用另一个函数,则会发生一样的事情。代码的执行流程进入函数内部,该函数建立一个新的执行上下文
,该上下文被推送到现有栈的顶部。浏览器将始终执行位于栈顶部的当前执行上下文
,而且一旦函数完成当前执行上下文
,它将从栈顶弹出,将控制权返回当前栈的栈顶上下文。下面的例子展现了递归函数和其程序的执行栈
:函数
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
复制代码
上面代码只调用自身3次,将i的值递增1。每次调用函数foo
时,都会建立一个新的执行上下文。一旦上下文执行完毕,它就会弹出栈而且将控制权返回它下面的上下文,直到再次到达全局上下文
。ui
关于执行栈
有五个关键点:this
执行上下文
,甚至是调用自身因此,咱们如今知道每次调用一个函数时,都会建立一个新的执行上下文
。可是,在JavaScript的解释器中,执行上下文
的调用都有两个阶段:
能够将每一个执行上下文
在概念上标示为具备3个属性的对象:
executionContextObj = {
'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
'this': {}
}
复制代码
调用函数时,但在执行实际函数以前,会建立此executionContextObj
。这被称为阶段1,即建立阶段
。这里,解释器经过扫描传入的参数或参数的函数、本地函数声明和局部函数声明来建立executionContextObj
。此扫描的结果将称为executionContextObj
中的variableObject
。
如下是解释器如何评估代码的伪概述:
函数
代码以前,建立执行上下文
。做用域链
变量对象
:
arguments对象
,检查参数的上下文,初始化名称和值并建立引用的副本。变量对象(或活动对象)
中建立一个属性,该属性是确切的函数名称,该函数具备指向内存中函数的引用指针。变量对象(或活动对象)
中建立一个属性,该属性是变量名称,并将值初始化为undefined。变量对象(或活动对象)
中,则不执行任何操做并继续扫描(即跳过)。看下下面的例子:
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等知识点,我将在下一篇文章中介绍,更多地关注与执行环境
相关的做用域链。
原文: http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
文章首发:https://github.com/reng99/blogs/issues/11
更多内容:https://github.com/reng99/blogs