执行上下文 执行上下文 可以理解为当前代码的执行环境,同一个函数在不同的环境中执行,会因为访问数据的不同产生不一样的结果。 执行上下文分为三种: 全局执行上下文:只有一个,程序首次运。..
一、执行上下文基础
1.1 执行上下文
执行上下文分为三种:
全局执行上下文:只有一个,程序首次运行时创建,它会在浏览器中创建一个全局对象(window
对象),使this
指向这个全局对象
函数执行上下文:函数被调用时创建,每次调用都会为该函数创建一个新的执行上下文
Eval 函数执行上下文:运行eval
函数中的代码时创建的执行上下文,少用且不建议使用
1.2 执行上下文栈
1 | 执行上下文栈(Execution context stack,ECS),也叫函数调用栈( |
call stack
1 | ),是一种拥有 |
LIFO
(后进先出)数据结构的栈,用于存储代码执行时创建的执行上下文
由于JS是单线程的,每次只能做一件事情,通过这种机制,我们能够追踪到哪个函数正在执行,其他函数在调用栈中排队等待执行。
JS引擎第一次执行脚本时,会创建一个全局执行上下文压到栈顶,然后随着每次函数的调用都会创建一个新的执行上下文放入到栈顶中,随着函数执行完毕后被执行上下文栈顶弹出,直到回到全局的执行上下文中。
代码实例
1 | var color = 'blue'; |
为全局上下文栈;其余为函数上下文栈
1 | 图解:  |
执行过程:
首先创建了全局执行上下文
,压入执行栈,其中的可执行代码开始执行。
然后调用changeColor
函数,JS引擎停止执行全局执行上下文,激活函数
创建它自己的执行上下文,且把该函数上下文放入执行上下文栈顶,其中的可执行代码开始执行。
调用了swapColors
函数,此时暂停了
的执行上下文,创建了
函数的新执行上下文,且把该函数执行上下文放入执行上下文栈顶。
当
函数执行完后,其执行上下文从栈顶出栈,回到了
执行上下文中继续执行。
没有可执行代码,也没有再遇到其他执行上下文了,将其执行上下文从栈顶出栈,回到了
中继续执行。
一旦所有代码执行完毕,JS引擎将从当前栈中移除
。
注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。
使用ECStack
1 | 来模拟调用栈: `ECStack=[]` JS第一次执行代码时就会遇到全局代码,执行上下文栈会压入一个全局上下文,我们用 |
globalContext
表示它,只有当整个应用程序结束的时候,
才会被清空,所以
最底部永远有个
1 | : `ECStack.push(globalContext)` 使用伪代码模拟上述代码行为: |
1 | ECStack.push(<changeColor> functionContext); |
因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到f2执行时,才创建了一个新的。具体演变过程如下。
1.3 es3版本
es3版本执行上下文内有三个重要属性:
变量对象 VO(variable object)
作用域链(scope chain)
this
可以将每个执行上下文抽象为一个对象。
执行上下文的组成代码示例:
1 | executionContextObj = { |
变量对象
变量对象 是与执行上下文相联的数据作用域,用来存储上下文中定义的变量和函数声明。
不同执行上下文中的变量对象也不一样:
全局上下文 中的变量对象就是全局对象,在浏览器中就是 window 对象。在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。所有的全局变量和函数都是作为 window 的属性和方法存在。
1 | console.log(this) //window |
1 |
|
而JavaScript采用的是词法作用域,fn 函数创建的作用域在函数定义时就已经确定了;
1 | 关联 : |
作用域只是一个“地盘”,其中没有变量,要通过作用域对应的执行上下文环境来获取变量的值,所以作用域是静态观念的,而执行上下文环境是动态的。也就是说,作用域只是用于划分你在这个作用域里面定义的变量的有效范围,出了这个作用域就无效。
同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。
生命周期
执行上下文的生命周期有三个阶段,分别是:
创建阶段
生成变量对象
创建arguments
扫描函数声明
扫描变量声明
建立作用域链
确定this的指向
执行阶段
变量赋值
函数的引用
执行其他代码
销毁阶段
创建阶段
1 | 生成变量对象 |
创建arguments:如果是函数上下文,首先会创建arguments
对象,给变量对象添加形参名称和值。
扫描函数声明:对于找到的函数声明,将函数名和函数引用(指针)存入VO
中,如果VO
中已经有同名函数,那么就进行覆盖(重写引用指针)。
扫描变量声明:对于找到的每个变量声明,将变量名存入VO
中,并且将变量的值初始化为undefined
。如果变量的名字已经在变量对象里存在,不会进行任何操作并继续扫描。
让我们举一个栗子来说明 :
1 | function age) { |
};
1 | function name() { |
}
1 | function getAge() { |
}
1 | console.log(typeof name); // string |
}
1 | person(20); |
`在调用person(20)的时候,但是代码还没执行的时候,创建的状态是这样:```javascript
1 | personContext = { |
0: 20,
1 | length: 1 |
},
1 | age: 20, |
},
1 | this: { ... } |
}
1 |
|
在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。
当书写一段函数代码时,就会创建一个词法作用域,这个作用域是函数内部的属性,我们用[[scope]]
表示,它里面保存父变量对象,所以
就是一条层级链。
person.[[scope]] = [
globalContext.variableObject
]
1 |
当函数调用,就意味着函数被激活了,此时创建函数上下文并压入执行栈,然后复制函数 [[scope]] 属性创建作用域链:
1 |
scopeChain:person.[[scope]]
1 | } |
scopeChain:[activationObject,[[scope]]]
1 | } |
如果当前函数被作为对象方法调用或使用bind
、call
、apply
等API
进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。
执行阶段
此时代码从上到下执行的时候激活阶段的过程是:
第一次执行console.log
; 此时name
在VO
中是函数。getName
未指定值在VO
中的值是
。
执行到赋值代码,
被赋值成函数表达式,
被赋值为abby
第二次执行
; 此时的
由于函数被字符串赋值覆盖因此是string
类型
是function
类型。
第三次执行
; 此时的
由于又被覆盖因此是
类型
因此理解执行上下文之后很好解释了变量提升(Hoisting):实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被JavaScript引擎放入内存中
这就解释了为什么我们能在
声明之前访问它,为什么之后的
的类型值发生了变化,为什么
第一次打印的时候是
等等问题了。
ES6 引入了let
和const
关键字,从而使 JavaScript 也能像其他语言一样拥有了块级作用域,很好解决了变量提升带来的一系列问题。
最后执行 console 时候的函数执行上下文:
},
hobby: 'game',
getName:pointer, pointer to function getName(),
},
}
1 |
|
}
1 | return f(); |
}
1 | checkscope(); |
`1、执行全局代码,生成全局上下文,并且压入执行栈```java
1 | ECStack=[ |
globalContext
]
复制代码
`2、全局上下文初始化```java
1 | globalContext={ |
}
1 |
|
5、此时 checkscope 函数还未执行,进入执行上下文
1 | 复制函数 [[scope]] 属性创建作用域链 |
用 arguments 属性创建活动对象
初始化变量对象,加入变量声明、函数声明、形参
活动对象压入作用域链顶端
1 | checkscopeContext = { |
},
1 | scope: undefined, |
},
1 | scopeChain: [activationObject, globalContext.variableObject], |
}
`6、checkscope 函数执行,对变量 scope 设值```javascript
},
1 | scope: 'local scope', |
},
}
`f 函数被创建生成 [[scope]] 属性,并保存父作用域的作用域链```java
1 | f.[[scope]]=[ |
checkscopeContext.activationObject,
]
`7、f 函数调用,生成 f 函数上下文,压栈```java
checkscopeContext,
fContext
]
1 |
|
9、f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
10、f 函数执行完毕,f函数上下文从执行上下文栈中弹出
]
`11、checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出```java
]
`示例二:```java
}
1 | return f; |
}
1 | checkscope()(); |
1 |
|
中的值,又正是因为JS实现了在子上下文引用父上下文的变量的时候,不会销毁这些变量的效果实现了闭包 这个概念!
1.4 es5版本
ES5 规范去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。
es5 执行上下文的生命周期也包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段做了三件事:
确定 this 的值,也被称为 This Binding
LexicalEnvironment(词法环境) 组件被创建
1 | VariableEnvironment(变量环境) 组件被创建 |
伪代码大概如下:
1 | ExecutionContext = { |
}
This Binding```text ThisBinding是和执行上下文绑定的,也就是说每个执行上下文中都有一个text this `,与text
es3
this
`并没有什么区别,```text
this
1 | 的值是在执行的时候才能确认,定义的时候不能确认 |
可以看到词法环境有两种类型 :
全局环境:是一个没有外部环境的词法环境,其外部环境引用为null
。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,
的值指向这个全局对象。
函数环境:用户在函数中定义的变量被储环境记录中,包含了
对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
词法环境有两个组件 :
环境记录器 :存储变量和函数声明的实际位置。
外部环境的引用 :它指向作用域链的下一个对象,可以访问其父级词法环境(作用域),作用与 es3 的作用域链相似
环境记录器也有两种类型 :
在函数环境中使用 声明式环境记录器,用来存储变量、函数和参数。
在全局环境中使用 对象环境记录器,用来定义出现在全局上下文中的变量和函数的关系。
因此:
创建全局上下文的词法环境使用 对象环境记录器 ,outer
值为
;
创建函数上下文的词法环境时使用 声明式环境记录器 ,
值为全局对象,或者为父级词法环境(作用域)
创建变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
在 ES6 中,词法环境和 变量环境的区别在于前者用于存储函数声明和变量(let
和
关键字)绑定,而后者仅用于存储变量(var
)绑定,因此变量环境实现函数级作用域,通过词法环境在函数作用域的基础上实现块级作用域。
🚨 使用let
/
声明的全局变量,会被绑定到Script
对象而不是Window
对象,不能以Window.xx
的形式使用;使用var
声明的全局变量会被绑定
对象;使用var
/let
/
声明的局部变量都会被绑定到Local
对象。注:
对象、
对象、
对象三者是平行并列关系。
箭头函数没有自己的上下文,没有arguments,也不存在变量提升
使用例子进行介绍
1 | let a = 20; |
1 | function multiply(e, f) { |
}
1 | c = multiply(20, 30); |
遇到调用函数```text multiply时,函数执行上下文开始被创建:```xml
1 | GlobalExectionContext = { |
1 | ThisBinding: <Global Object>, |
1 | LexicalEnvironment: { |
}
1 | outer: <null> |
},
1 | VariableEnvironment: { |
}
}
1 | FunctionExectionContext = { |
1 | Type: "Declarative", |
},
1 | outer: <GlobalLexicalEnvironment> |
},
1 | g: undefined |
},
}
变量提升的原因:在创建阶段,函数声明存储在环境中,而变量会被设置为```text(在 var 的情况下)或保持未初始化```text
uninitialized
1 | (在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。 |
在 showName 内部查找 myname 时会先使用当前函数执行上下文里面的变量 myname ,由于变量提升
,当前的执行上下文中就包含了变量 myname,而值是 undefined,所以获取到的 myname 的值就是 undefined。
在此阶段,完成对所有这些变量的分配,最后执行代码,如果
引擎不能在源码中声明的实际位置找到let
变量的值,它会被赋值为
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
过程总结
创建阶段 首先创建全局上下文的词法环境:首先创建对象环境记录器
,接着创建他的外部环境引用
,值为 null
创建全局上下文的语法环境:过程同上
确定 this 值为全局对象(以浏览器为例,就是 window )
函数被调用,创建函数上下文的词法环境:首先创建声明式环境记录器
,值为 null,值为全局对象,或者为父级词法环境
创建函数上下文的变量环境:过程同上
确定 this 值
进入函数执行上下文的 执行阶段
执行完成后进入 回收阶段
实例讲解
将词法环境中
抽离出来,执行上下文结构如下:
下面我们以如下示例来分析执行上下文的创建及执行过程:
1 | function ){ |
{
1 | let b = 3 |
}
1 | console.log(c) |
}
1 | foo() |
第一步: 调用```text foo函数前先编译并创建执行上下文,在编译阶段将```text
1 | var |
`声明的变量存放到变量环境中,```text
1 | let |
`声明的变量存放到词法环境中,需要注意的是此时在函数体内部块作用域中```text
1 | let |
1 | 声明的变量不会被存放到词法环境中,如下图所示 : |
当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量, 比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的行上下文中查找。为了直观理解,你可以看下面这张图:
从图中可以看出,bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。 现在你知道变量是通过作用域链来查找的了,不过还有一个疑问没有解开,foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?
结合变量环境、词法环境以及作用域链,我们看下下面的代码:
1 | var myName = " 极客世界 " |
}
1 | let test = 2 |
{
1 | let test = 3 |
}
1 | let myAge = 10 |
对于上面这段代码,当执行到 bar 函数内部的 if 语句块时,其调用栈的情况如下图所示:
解释下这个过程。首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。
本文标题: 带你搞懂执行上下
发布时间: 2019年02月01日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/4438e1d7/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

