执行上下文
又名执行环境(Execution Context 简称EC),执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文分为:
- 全局执行上下文
这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文
每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- Eval 函数执行上下文(不常用)
执行栈
执行上下文栈(Execution Context Stack),也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
执行上下文生命周期
执行上下文分两个阶段创建:1.创建阶段,2.执行阶段(完成变量分配,执行代码);
ES3版本和ES5版本有些不同。
ES3
在创建阶段会发生3件事:
- 全局上下文中的变量对象就是全局对象。
- 在函数上下文中,使用 活动对象(Activation object, AO) 来表示变量对象。
- 由于变量对象是规范上的或者说是引擎实现的,不可在 JS 环境中访问,只有当进入一个执行上下文中,这个执行上下文的 变量对象(VO) 才会被 激活 变成 活动对象(AO)
- 活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。
创建变量对象的过程大致为:函数的形参 > 函数声明 > 变量声明,
其中在创建函数声明时,如果名字存在,则会被重写,在创建变量时,如果变量名存在,则忽略不会进行任何操作。
function foo(a){
var b = 2;
function c() {}
var d = function() {};
}
foo(1);
// 在进入执行上下文后,这时的 AO 为
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: referece to function c(){},
d: undefined;
}
总结
- 全局上下文的变量对象初始化是全局对象。
- 函数上下文的变量对象初始化只包括 Arguments 对象。
- 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值。
- 在后续的代码执行阶段,会再次修改变量对象的属性值。
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
函数有一个内部属性[[scope]]
,当函数创建的时候,就会保存所有父变量对象到其中,但并不代表 [[scope]] 是完整的作用域链。
举个例子:
function foo(){
function bar(){}
}
函数创建时,各自的 [[scope]] 为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
]
ES5
在任意的 JavaScript 代码被执行前,执行上下文处于创建阶段。在创建阶段中总共发生了三件事情:
- 确定 this 的值,也被称为 This Binding。
- LexicalEnvironment(词法环境) 组件被创建。
- VariableEnvironment(变量环境) 组件被创建。
词法环境
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
简而言之,词法环境是一个包含标识符变量映射的结构。
(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)。
词法环境有两种类型:
- 全局环境(在全局执行上下文中)是一个没有外部环境的词法环境。全局环境的外部环境引用为 null。它拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量,
this
的值指向这个全局对象。 - 函数环境,用户在函数中定义的变量被存储在环境记录中。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
注意: 对于函数环境而言,环境记录还包含了一个 arguments
对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的长度。
在词法环境中,有两个组成部分:
(1)环境记录(environment record) :环境记录是存储变量和函数声明的实际位置。
(2)对外部环境的引用(outer):对外部环境的引用意味着它可以访问其外部词法环境。
环境记录同样有两种类型:
- 声明性环境记录: 存储变量、函数和参数。一个函数环境包含声明性环境记录。
- 函数环境记录:用于函数作用域。
- 模块环境记录:模块环境记录用于体现一个模块的外部作用域,即模块export所在环境。
- 对象环境记录: 用于定义在全局执行上下文中出现的变量和函数的关联。全局环境包含对象环境记录。
词法环境与我们自己写的代码结构相对应,也就是我们自己代码写成什么样子,词法环境就是什么样子。词法环境是在代码定义的时候决定的,跟代码在哪里调用没有关系。所以说JavaScript采用的是词法作用域(静态作用域)。
举例:
var a = 2;
let x = 1;
const y = 5;
function foo() {
console.log(a);
function bar() {
var b = 3;
console.log(a * b);
}
bar();
}
function baz() {
var a = 10;
foo();
}
baz();
// 全局词法环境
GlobalEnvironment = {
outer: null, //全局环境的外部环境引用为null
GlobalEnvironmentRecord: {
//全局this绑定指向全局对象
[[GlobalThisValue]]: ObjectEnvironmentRecord[[BindingObject]],
//声明式环境记录,除了全局函数和var,其他声明都绑定在这里
DeclarativeEnvironmentRecord: {
x: 1,
y: 5
},
//对象式环境记录,绑定对象为全局对象
ObjectEnvironmentRecord: {
a: 2,
foo:<< function>>,
baz:<< function>>,
isNaNl:<< function>>,
isFinite: << function>>,
parseInt: << function>>,
parseFloat: << function>>,
Array: << construct function>>,
Object: << construct function>>
...
...
}
}
}
//foo函数词法环境
fooFunctionEnviroment = {
outer: GlobalEnvironment,//外部词法环境引用指向全局环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
bar:<< function>>
}
}
//bar函数词法环境
barFunctionEnviroment = {
outer: fooFunctionEnviroment,//外部词法环境引用指向foo函数词法环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
b: 3
}
}
//baz函数词法环境
bazFunctionEnviroment = {
outer: GlobalEnvironment,//外部词法环境引用指向全局环境
FunctionEnvironmentRecord: {
[[ThisValue]]: GlobalEnvironment,//this绑定指向全局环境
a: 10
}
}
变量环境
- 它也是一个词法环境,其
EnvironmentRecord
包含了由 VariableStatements 在此执行上下文创建的绑定。 - 如上所述,变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性。
- 在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量(
let
和const
)绑定,而后者仅用于存储变量(var
)绑定。
举例:
let a = 20;
const b = 30;
var c;
function multiply(e, f){
var g = 20;
return e*f*g;
}
c = multiply(20, 30);
// 全局执行上下文
GlobalExectionContext = {
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定,let、const、函数声明
a: <uninitialized>,
b: <uninitialized>,
multiply:< func >
}
outer: <null>
},
// 变量环境
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 标识符绑定,var 声明
c: undefined,
}
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定
Arguments: { 0:20, 1:30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
注意:只有遇到调用函数 multiply
时,函数执行上下文才会被创建。
- 在执行上下文创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined (在 var 的情况下)或保持未初始化 uninitialized(在 let 和 const 的情况下)。
- 这是因为在创建阶段,代码会被扫描并解析变量和函数声明,其中函数声明存储在环境中,而变量会被设置为 undefined,这就是我们所谓的变量提升
- 这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined),但是如果在声明前访问 let 和 const 定义的变量就会提示引用错误的原因(也就是我们所常说的暂时性死区(TDZ,temporal dead zone))。
代码执行步骤:
- 1:创建一个新的执行上下文(Execution Context)
- 2:创建一个新的词法环境(Lexical Environment)
- 3:把 LexicalEnvironment 和 VariableEnvironment 指向新创建的词法环境
- 4:把这个执行上下文压入执行栈并成为正在运行的执行上下文
- 5:执行代码
- 6:执行结束后,把这个执行上下文弹出执行栈
https://juejin.cn/post/6844903745965260807 https://zhuanlan.zhihu.com/p/48590085
作用域
- 之前只有全局作用域和函数作用域,ES6有了块级作用域。
- JavaScript采用词法作用域(lexical scoping),也就是静态作用域。词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
- 作用域链:其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。
作用域和执行上下文:
- 作用域在于指定变量、函数的作用范围,即它们可以在什么范围内被访问到,也就是它们的可访问性。
- 执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。
- 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
上下文环境可以理解为一个看不见但实际存在的对象,所有变量都在里存着(所以在调用时创建就很好理解了,拿参数做例子,你不调用函数,我怎么知道你要给我传什么参数?) 而作用域比较抽象,创建一个函数就创建了一个作用域,无论你调用不调用,函数只要创建了,它就有独立的作用域,就有自己的一个“地盘”。 一个作用域下可能包含若干个上下文环境;也有可能从来没有上下文环境(函数未被调用执行);也有可能有过,但是函数执行后,上下文环境被销毁了。