企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] >http://blog.csdn.net/z742182637/article/category/6047401/2 # 深入理解Javascript之执行上下文(Execution Context) 在这篇文章中,将比较深入地阐述下执行上下文 - Javascript 中最基础也是最重要的一个概念。相信读完这篇文章后,你就会明白 javascript 引擎内部在执行代码以前到底做了些什么,为什么某些函数以及变量在没有被声明以前就可以被使用,以及它们的最终的值是怎样被定义的。 ## 什么是执行上下文(**EC**) Javascript中代码的运行环境分为以下三种: * 全局级别的代码 - 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。 * 函数级别的代码 - 当执行一个函数时,运行函数体中的代码。 * `eval`的代码 - 在`eval`函数内运行的代码。 在网上可以找到很多阐述作用域的资源,为了使该文便于大家理解,我们可以将“执行上下文”看做当前代码的运行环境或者作用域。 下面我们来看一个示例,其中包括了全局以及函数级别的执行上下文: ![](https://box.kancloud.cn/b6addd1c134b1034fdacb797db2e651c_554x447.png) 上图中,一共用4个执行上下文。 * <i style="color:#BE00B3">紫色的代表全局的上下文;</i> * <i style="color:#22CC01">绿色代表person函数内的上下文;</i> * <i style="color:#0036FF">蓝色</i>以及<i style="color:#FF9600">橙色</i>代表 person 函数内的另外两个函数的上下文。 注意,不管什么情况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,我们可以在 person 的上下文中访问到全局上下文中的 sayHello 变量,当然在函数 firstName 或者 lastName 中同样可以访问到该变量。 ## 执行上下文堆栈(ECS) 一系列活动的执行上下文从逻辑上形成一个栈。**栈底总是全局上下文,栈顶是当前(活动的)执行上下文**。当在不同的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(通过压栈或者退栈的形式)。 **压栈:**全局EC—>局部EC1—>局部EC2—>当前EC **出栈:**全局EC<—局部EC1<—局部EC2<—当前EC 我们可以用数组的形式来表示环境栈: ``` ECS=[局部EC,全局EC]; ``` 每次控制器进入一个函数(哪怕该函数被递归调用或者作为构造器),都会发生压栈的操作。过程类似 javascript 数组的 push 和 pop 操作。 在浏览器中,javascript 引擎的工作方式是单线程的。也就是说,某一时刻**只会有一个事件是被激活处理的**,其它的事件被放入队列中,等待被处理。 下面的示例图描述了这样的一个堆栈: ![](https://box.kancloud.cn/df4e634fd5133eb8ae456e6ad5bd880c_555x288.png) 我们已经知道,**当javascript代码文件被浏览器载入后,默认最先进入的是一个全局的执行上下文**。 当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数创建一个新的执行上下文,并且将其压入到执行上下文堆栈的顶部。 **浏览器总是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,然后,进入其下的上下文执行代码。last-in first-out stack (LIFO stack)** 这样,堆栈中的上下文就会被依次执行并且弹出堆栈,直到回到全局的上下文。请看下面一个例子: ```js (function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0)); ``` 上述 `foo` 被声明后,通过 `()` 运算符立即执行运行了。函数代码就是调用了其自身3次,每次是局部变量 `i` 增加1。每次 `foo` 函数被自身调用时,就会有一个新的执行上下文被创建。每当一个上下文执行完毕,该上上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。整个过程抽象如下图: ![](https://box.kancloud.cn/b41621c52e822cb1754d99ab14e6fcfa_390x259.png) 由此可见 ,对于执行上下文这个抽象的概念,可以归纳为以下几点: * 单线程 * 同步执行 * 唯一的一个全局上下文 * 函数的执行上下文的个数没有限制 * 每次函数被调用创建新的执行上下文,包括调用自己。 ## 执行上下文的建立过程 我们现在已经知道,**每当调用一个函数时,一个新的执行上下文就会被创建出来**。 JavaScript 代码自上而下执行,但是在 js 代码执行前,javascript 引擎内部会首先进行词法分析,所以事实上,js 运行要分为**预编译的词法分析**和**实际执行**两个阶段: ### 预编译阶段(进入上下文阶段,会进行一系列的词法分析,发生在当调用一个函数时,但是在执行函数体内的具体代码以前) ![](https://box.kancloud.cn/5c6be302e9d98ca677c4d93df4245f65_387x315.png) * 建立变量,函数,`arguments` 对象,参数; * 建立作用域链; * 确定 this 的值; #### 创建变量对象 **创建变量对象**主要是经过以下过程,如图所示: ![](https://box.kancloud.cn/33c89d0faf54f4076bca04eae4e284ca_590x232.png) 1. 创建 `arguments` 对象,检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行的,全局环境没有此过程。 2. 检查当前上下文的**函数声明**,按照代码顺序查找,将找到的函数提前声明,如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在**堆内存地址引用**,如果存在,则会被新的引用覆盖掉。 3. 检查当前上下文的**变量声明**,爱去哪找代码顺序查找,将找到的变量提前声明,如果当前上下文的变量对象没有变量名属性,则在该变量对象以变量名建立一个属性,属性值为 `undefined`;如果存在,则忽略该变量声明。 **函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明**。 **创建变量对象发生在预编译阶段,还没有进入到执行阶段,该变量对象都不能访问的**,因为此时的变量对象中的变量属性尚未赋值,值仍为 `undefined`,只有在进行执行阶段,变量中的变量属性才进行赋值后,变量对象(Variable Object)转为活动对象(Active Object)后,才能进行访问,这个过程就是 VO -> AO 过程。 ### 执行阶段 变量赋值,函数引用,执行其它代码; 实际上,可以把执行上下文看做一个对象,其下包含了以上3个属性: ~~~ executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ }, this: {} } ~~~ > 进入执行上下文时,**VO**(variableObject)的初始化过程具体如下: > 函数的形参(当进入函数执行上下文时)—— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为 `undefined`; > 函数声明(FunctionDeclaration, **FD**) —— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;**如果变量对象已经包含了相同名字的属性,则替换它的值**; > 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为 `undefined`;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。 **注意:该过程是有先后顺序的。** > 执行代码阶段时,VO 中的一些属性 `undefined` 值将会确定。 ## **建立阶段以及代码执行阶段的详细分析** 确切地说,执行上下文对象(上述的 executionContextObj )是在函数被调用时,但是在函数体被真正执行以前所创建的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 - 建立阶段。 这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,然后基于这些信息建立执行上下文对象(executionContextObj)。 在这个阶段,variableObject 对象,作用域链,以及 this 所指向的对象都会被确定。 ### AO 活动对象 在函数的执行上下文中,VO 是不能直接访问的。它主要扮演被称作活跃对象(activation object)(简称:**AO**)的角色。 这句话怎么理解呢,就是当 EC 环境为函数时,我们访问的是 AO,而不是 VO。 不理解,可以看一下 JavaScript高级程序设计的原话: ~~~ function compare(value1,value2){ if (value1<value2){ return -1; } else if (value1>value2){ return 1; } else { return 0; } } var result = compare(5,10) ~~~ 以上代码定义了`compare()`函数,然后又在全局作用域中调用了它(定义了变量`result`,赋值`compare(5,10)`) 当调用`compare()`时,会创建一个包含`arguments`、`value1`、`value2`的 **活动对象**。 全局执行环境的**变量对象**(包含`result`和`compare`)。 也就是说:在全局环境中,没有了所谓的**活动对象**(AO)概念,当我们理解一个**函数的运行时**,我们就需要**变量对象**(VO)来帮助我们理解,但此时我们已经不太关心**活动对象**(AO)了,并不是它不存在了。 ``` VO(functionContext) === AO; ``` AO 是在进入函数的执行上下文时创建的,并为该对象初始化一个`arguments`属性,该属性的值为`arguments`对象。 ``` AO = { arguments: { callee:, length:, properties-indexes: //函数传参参数值 } }; ``` FD 的形式只能是如下这样: ```js function f(){ } ``` 当函数被调用是 executionContextObj 被创建,但在实际函数执行之前。这是我们上面提到的第一阶段,创建阶段。在此阶段,解释器扫描传递给函数的参数或 arguments,本地函数声明和本地变量声明,并创建 executionContextObj 对象。扫描的结果将完成变量对象的创建。 上述第一个阶段的具体过程如下: 1. 找到当前上下文中的调用函数的代码 2. 在执行被调用的函数体中的代码以前,开始创建执行上下文 3. 进入第一个阶段-建立阶段: 建立variableObject对象: 1. 建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值 2. 检查当前上下文中的函数声明: 每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用。 如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。 3. 检查当前上下文中的变量声明: 每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。 如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。 初始化作用域链 确定上下文中 this 的指向对象 4. 代码执行阶段: 执行函数体中的代码,一行一行地运行代码,给`variableObject`中的变量属性赋值。 下面来看个具体的代码示例: ```js function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22); ``` 在调用`foo(22)`的时候,建立阶段如下: ``` fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c:<pointer to function c()> a: undefined, b: undefined }, scopeChain: { ... }, this: { ... } } ``` 由此可见,在建立阶段,除了`arguments`,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下: ``` fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: <pointer to function privateB()> }, scopeChain: { ... }, this: { ... } } ``` 我们看到,**只有在代码执行阶段,变量属性才会被赋予具体的值**。 ## 局部变量作用域提升的缘由 在网上一直看到这样的总结: 在函数中声明的变量以及函数,其作用域提升到函数顶部,换句话说,就是一进入函数体,就可以访问到其中声明的变量以及函数。这是对的,但是知道其中的缘由吗?相信你通过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码: ```js (function() { console.log(typeof foo); // function console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } console.log(typeof foo); // string console.log(typeof bar); // function }()); ``` 上述代码定义了一个匿名函数,并且通过 `()` 运算符强制理解执行。那么我们知道这个时候就会有个执行上下文被创建,我们看到例子中马上可以访问 `foo` 以及 `bar` 变量,并且通过 `typeof` 输出 `foo` 为一个函数引用,`bar` 为 `undefined`。 **为什么我们可以在声明 foo 变量以前就可以访问到 foo 呢?** 因为在上下文的建立阶段,先是处理 `arguments`, 参数,接着是函数的声明,最后是变量的声明。那么,发现 `foo`函数的声明后,就会在variableObject 下面建立一个 `foo` 属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有 `var foo` 的声明,但是 variableObject已经具有了 `foo` 属性,所以直接跳过。当进入代码执行阶段的时候,就可以通过访问到 `foo` 属性了,因为它已经就存在,并且是一个函数引用。 **为什么 `bar` 是 `undefined` 呢?** 因为`bar`是变量的声明,在建立阶段的时候,被赋予的默认的值为 `undefined`。由于它只要在代码执行阶段才会被赋予具体的值,所以,当调用 `typeof(bar)` 的时候输出的值为 `undefined`。 到此,相信你应该对执行上下文有所理解了,这个执行上下文的概念非常重要,务必好好搞懂之! # 谁先被提升? 再来个例子,`foo` 是先提升变量声明 还是 函数声明 ? ```js console.log(typeof foo); // function var foo = "this is var foo"; function foo() { console.log("this is function foo"); } console.log(typeof foo) // string ``` `foo` 函数应该是先被整体提升(不是 `undefined`),然后才是 变量提升。 # 参考 《测试驱动的JavaScript开发》-第五章 函数