执行上下文
什么是执行上下文
:
执行上下文是当前JavaScript代码被解析和执行时所在环境的抽象概念。
执行上下文有哪些类型:
执行上下文总共有三种类型
全局执行上下文:
浏览器中的全局对象就是window对象,this指向这个全局对象。函数执行上下文
:存在无数个,只有在函数被调用时才会被创建,每次调用函数都会创建一个新的执行上下文。Eval函数执行上下文
:指的是运行Eval函数中的代码,很少使用而且不建议。
执行栈
执行栈也叫调用栈,具有数据结构的栈的先入后出的规则,用于存储在代码执行期间所创建的所有执行上下文。
首次运行JS代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每次发生函数调用,引擎就会为该函数创建一个新的函数执行上下文,并Push到当前执行栈的栈顶。
根据执行栈的先进后出规则,当前栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。
一个函数调用仅具有一个与其相关联的执行上下文。
执行上下文的创建
执行上下文 也可以说是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this
的值,以及其它的一些内部细节。
两个阶段:创建阶段、执行阶段
创建阶段:
确定this的值
,也被称为This BindingLexicalEnviroment
(词法环境)被创建。VariableEnvironment
(变量环境)被创建。
数据结构伪代码表示:
ExecutionContent = {
ThisBinding : <this.value>,//确定this
LexicalEnviroment : { ..... },//词法环境
VariableEnvironment : { ..... },//变量环境
}
This Binding
- 全局执行上下文中,this指向全局对象,在浏览器中指向window对象,而在nodejs中指向文件的module对象。
- 函数执行上下文中,this的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显示绑定、new绑定、箭头函数等等。
this
的值就是在点之前的这个对象,即调用该方法的对象。
在 JavaScript 中,this
是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象。
在运行时对 this
求值的这个概念既有优点也有缺点。一方面,函数可以被重用于不同的对象。另一方面,更大的灵活性造成了更大的出错的可能。
LexicalEnviroment
有两种类型:
- 全局环境:是一个没有外部环境的词法环境,其外部环境引用的
null
。拥有一个全局对象(window)及相关联的属性方法(列如数组方法)以及用户自定义的全局变量,this指向这个全局对象。 - 函数环境:用户在函数里面定义的变量(被存储到环境记录中),包含了argument对象。对外部环境的引用可以是全局环境或者也可以是包含内部函数的外部函数环境。
数据结构伪代码表示:
GlobalExecutionContent = { //全局执行上下文
ThisBinding : <Global Object>,//确定this
LexicalEnviroment : {//词法环境
EnvironmentRecord : {//环境记录
Type : "Object",//全局环境
}
//标识符绑定在这里
outer : <null> //对外部环境的引用
}
}
FunctionExecutionContent = {//函数执行上下文
ThisBinding : <Global Object>,//确定this
LexicalEnviroment : {//词法环境
EnvironmentRecord : {//环境记录
Type : "Declarative",//函数环境
}
//标识符绑定在这里
outer : < Global or outer function environment reference> //对外部环境的引用
}
}
VariableEnvironment
-
变量环境也是一种词法环境,因此它具有以上所定义的词法环境的所有属性。
-
在ES6中,词法环境和变量环境的区别在于前者用于存储 函数声明和变量(let、const)绑定,而后者仅用于存储变量(var)绑定
//全局执行上下文
GlobalExectionContext = {
// this绑定为全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
//环境记录
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 let const创建的变量a b在这
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
// 全局环境外部环境引入为null
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object", // 对象环境记录
// 标识符绑定在这里 var创建的c在这
c: undefined,
}
// 全局环境外部环境引入为null
outer: <null>
}
}
// 函数执行上下文
FunctionExectionContext = {
//由于函数是默认调用 this绑定同样是全局对象
ThisBinding: <Global Object>,
// 词法环境
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 arguments对象在这
Arguments: {0: 20, 1: 30, length: 2},
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative", // 声明性环境记录
// 标识符绑定在这里 var创建的g在这
g: undefined
},
// 外部环境引入记录为</Global>
outer: <GlobalEnvironment>
}
}
LexicalEnviroment
(词法环境深入):
- 环境记录:一个存储所有局部变量作为其属性(包括一些其他信息,例如
this
的值)的对象。存储变量和函数声明的实际位置。 - 对 外部词法环境 的引用,与外部代码相关联。可以访问其外部词法环境。
Step 1. 变量
- 这段没有函数的简单的代码中只有一个词法环境:
-
这就是所谓的与整个脚本相关联的 全局 词法环境。
-
在上面的图片中,矩形表示环境记录(变量存储),箭头表示外部引用。全局词法环境没有外部引用,所以箭头指向了
null
。 -
随着代码开始并继续运行,词法环境发生了变化。
这是更长的代码:
右侧的矩形演示了执行过程中全局词法环境的变化:
- 当脚本开始运行,词法环境预先填充了所有声明的变量。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
let
声明前,不能引用它。几乎就像变量不存在一样。
- 最初,它们处于“未初始化(Uninitialized)”状态。这是一种特殊的内部状态,这意味着引擎知道变量,但是在用
- 然后
let phrase
定义出现了。它尚未被赋值,因此它的值为undefined
。从这一刻起,我们就可以使用变量了。 phrase
被赋予了一个值。phrase
的值被修改。
- 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
- 操作变量实际上是操作该对象的属性。
“词法环境”
是一个规范对象(specification object):它仅仅是存在于 编程语言规范 中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。
但 JavaScript 引擎同样可以优化它,比如清除未被使用的变量以节省内存和执行其他内部技巧等,但显性行为应该是和上述的无差。
Step 2. 函数声明
-
一个函数其实也是一个值,就像变量一样。
-
不同之处在于函数声明的初始化会被立即完成。
-
当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像
let
那样直到声明处才可用)。 -
这就是为什么我们可以在(函数声明)的定义之前调用函数声明。
例如,这是添加一个函数时全局词法环境的初始状态:
正常来说,这种行为仅适用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如 let say = function(name)...
。
Step 3. 内部和外部的词法环境
在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。
例如,对于 say("John")
,它看起来像这样(当前执行位置在箭头标记的那一行上):
在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):
- 内部词法环境与
say
的当前执行相对应。它具有一个单独的属性:name
,函数的参数。我们调用的是say("John")
,所以name
的值为"John"
。 - 外部词法环境是全局词法环境。它具有
phrase
变量和函数本身。
内部词法环境引用了 outer
。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
如果在任何地方都找不到这个变量,那么在严格模式下就会报错(在非严格模式下,为了向下兼容,给未定义的变量赋值会创建一个全局变量)。
在这个示例中,搜索过程如下:
- 对于
name
变量,当say
中的alert
试图访问name
时,会立即在内部词法环境中找到它。 - 当它试图访问
phrase
时,然而内部没有phrase
,所以它顺着对外部词法环境的引用找到了它。
Step 4. 返回函数
看看 makeCounter
这个例子。
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
在每次 makeCounter()
调用的开始,都会创建一个新的词法环境对象,以存储该 makeCounter
运行时的变量。
因此,我们有两层嵌套的词法环境
不同的是,在执行 makeCounter()
的过程中创建了一个仅占一行的嵌套函数:return count++
。我们尚未运行它,仅创建了它。
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]]
的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
因此,counter.[[Environment]]
有对 {count: 0}
词法环境的引用。这就是函数记住它创建于何处的方式,与函数被在哪儿调用无关。[[Environment]]
引用在函数创建时被设置并永久保存。
稍后,当调用 counter()
时,会为该调用创建一个新的词法环境,并且其外部词法环境引用获取于 counter.[[Environment]]
:
现在,当 counter()
中的代码查找 count
变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 makeCounter()
的词法环境,并且在哪里找到就在哪里修改。
在变量所在的词法环境中更新变量。
这是执行后的状态:
如果我们调用 counter()
多次,count
变量将在同一位置增加到 2
,3
等。
参考:
变量作用域,闭包