本文将深入探讨JAVAScript中最重要的基础知识之一:执行上下文。通过对此篇文章的阅读,对以下几个方面的知识你将会有更加清晰的认识:
什么是执行上下文?
当代码在JS中运行时,代码的执行环境非常重要,JavaScript中可执行的代码分为以下几类:
我们可以在网上找到很多与作用域相关的文档等,本文为了便于知识点的理解,将执行上下文看作是当前代码执行所处的环境/作用域。下面是一个包括全局和函数上下文的代码示例:
以上示例代码结构很简明,一个由紫色实线包裹的全局上下文和三个分别由绿色、蓝色和橙色实线包裹的函数上下文。每个程序中只能有一个可被其他程序所访问的全局上下文。函数上下文可以有任意多个,并且每个函数在调用的时候都会产生一个新的函数上下文和一个私有的作用域,当前作用域中所声明的任何变量都不能被外部所直接访问或调用。上例中,函数可直接访问当前上下文外部声明的变量,但是外部函数上下文不能访问内部声明的变量或者函数。为何会出现这种情况呢?代码到底是怎么执行的呢?
执行环境栈
浏览器中JavaScript解释器的运行是单线程的。这也就意味着在浏览器中同一时刻只能做一件事情,其他行为或者事件需要在执行栈中排队等待。下图是对单线程的抽象展示:
当浏览器首次加载脚本语言的时候,会默认进入全局执行上下文。如果在全局代码中调用其他函数,当前程序的时序会自动进入所调用的函数中,与此同时会创建一个新的执行上下文并将其压入执行栈的顶部。如果在当前函数内部调用其他函数,执行过程如上所述。代码的执行流程会进入到内部函数中,创建一个新的执行上下文并将它压入执行栈的顶部。浏览器永远执行位于栈顶的执行上下文,并且一旦当前函数执行上下文执行结束,它将从栈顶弹出,执行控制权也会回到当前栈的新栈顶。这样,执行环境栈中的上下文就会被依次执行和弹出栈顶,直到回到全局上下文,下例所示:
(function foo(i){ if (i===3) { return; }else{ foo(++i); } }(0)) 复制代码
代码自调用三次,i的值不断从1自增。每次函数foo被调用的时候,一个新的执行上下文就会创建。一旦当前上下文执行结束,它就会从栈顶弹出,回到栈顶的新的上下文,直到再次回到全局上下文。
执行栈中需要记住的5个关键点:
详解执行上下文
截至目前我们已经知道每当一个函数被调用的时候,就会产生一个新的执行上下文。但是,在JavaScript解释器中,对每个执行上下文的调用都分为以下两个阶段:
因为可以将执行上下文概念性的描述为含有三个属性的对象:
executionContextObj = { 'scopeChain':{/*variableObject+所有父类执行上下文的variableObject*/}, 'variableObject':{/*函数形参/实参,内部的变量和函数声明*/}, 'this':{} } 复制代码
激活/变量对象[AO/VO]
执行上下文对象是在函数被调用,但是在函数被执行前所产生的。也就是上文所述的阶段1—创建阶段。比部分中,解释器对执行上下文对象的创建主要是通过浏览函数的实参和形参、当前函数内部的变量声明和函数声明。这部分的浏览结果会成为执行上下文对象中的变量对象。
解释器对代码执行的伪逻辑概述:
下面看一个例子:
function foo(i){ var a = 'hello', var b = function privateB(){ }, function c(){ } } foo(22); 复制代码
当调用函数foo的时候,创建阶段如下所示:
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); // undefiend var foo = 'hello', bar = function (){ return 'world'; }; function foo(){ return 'hello'; }; }()) 复制代码
对于疑问和解答如下:
注:以上部分译自此文,如有侵权请告知;如有翻译不妥,还请各位读者指正。以下是我对本文知识点的简要总结。
简要总结
function foo(i){ console.log(i); // function pointer var i = function (){ } } foo(2); 复制代码 变量对象初始化第一步:函数参数 复制代码 executionContextObj = { 'scopeChain':{...}, 'variableObject':{ arguments:{ 0:2, length: 1, }, i:2 } } 复制代码 变量对象初始化第二步:函数声明 函数声明过程中,变量对象中已存在同名的属性i,将其值由"1"替换为新值"function" 复制代码 executionContextObj = { 'scopeChain':{...}, 'variableObject':{ arguments:{ 0:2, length: 1, }, i: function (){ } } } 复制代码
function foo(i){ console.log(i); // function pointer var i = function (){ }, var i = 9; } foo(2); 复制代码 变量对象初始化第一步:函数参数 复制代码 executionContextObj = { 'scopeChain':{...}, 'variableObject':{ arguments:{ 0:2, length: 1, }, i:2 } } 复制代码 变量对象初始化第二步:函数声明 函数声明过程中,变量对象中已存在同名的属性i,将其值由‘1’替换为新值‘function’ 复制代码 executionContextObj = { 'scopeChain':{...}, 'variableObject':{ arguments:{ 0:2, length: 1, }, i: function (){ } } } 复制代码 变量对象初始化第三步:变量声明 变量声明过程中,变量对象中已存在同名的属性i,不进行任何操作。 复制代码 executionContextObj = { 'scopeChain':{...}, 'variableObject':{ arguments:{ 0:2, length: 1, }, i: function (){ } }, 'this':{...} } 复制代码