JavaScript 执行上下文和执行栈
EC(Execution Context)执行上下文
执行上下文的类型
JavaScript 中有三种类型的执行上下文:
- 全局执行上下文(GEC):这是默认或基本的执行上下文。不在任何函数内的代码都属于全局上下文,一个程序中只能有一个全局执行上下文。它要做的有两件事:
- 创建一个全局对象,在浏览器中为(
window
)。 - 将
this
的值设置为全局对象。
- 创建一个全局对象,在浏览器中为(
- 函数执行上下文(FEC):每次调用函数时,都会为该函数创建一个全新的执行上下文。每个函数都有自己的执行上下文,但它是在调用函数时创建的,这个上下文可以保护里面的私有变量和外界互不干扰。
- Eval 函数执行上下文(EFEC):在
eval
函数执行代码也会产生一种特殊的执行上下文,但由于我们通常不会使用它,因此就不讨论了。
每当一个函数执行完毕,则这个函数的执行上下文也将从栈中弹出,等到所有函数都运行完毕,要关闭页面的时候,全局上下文也将出栈释放,程序运行结束。如果当前上下文中的某些内容,被当前上下文以外的东西占用,那么当前上下文是不能被释放的,这就是我们熟知的闭包(Closure)。
ECS(Execution Context Stack)执行栈
执行栈也称为调用栈,具有 LIFO(后进先出)的结构,用于存储代码执行过程中创建的所有执行上下文。
当 JavaScript 引擎第一次遇到您的脚本时,它会创建一个全局执行上下文并将其推送到当前执行堆栈。每当引擎找到函数调用时,它都会为该函数创建一个新的执行上下文并将其推送到堆栈顶部。
举个例子:
let a = 1
function foo() {
bar()
}
function bar() {
console.log(a)
}
foo()
那么执行过程中执行栈的变化如下:
- 全局执行上下文创建,压入执行栈。
- 执行
foo()
,foo
函数执行上下文创建,压入执行栈。 - 执行
bar()
,bar
函数执行上下文创建,压入执行栈。 bar()
执行完毕,当前执行上下文出栈。foo()
执行完毕,当前执行上下文出栈。- 全局代码执行完毕,出栈,执行栈清空。
你可能遇到过以下错误:
Uncaught RangeError: Maximum call stack size exceeded
这表示你嵌套调用了太多次函数,以至于系统分配给浏览器的执行栈放不下更多的执行上下文了,通常在你进行了太多的递归调用时会发生:
function a() {
a()
}
a()
LE(Lexical Environment)词法环境
执行上下文是在创建阶段创建的。在创建阶段会发生以下事情:
- 创建了
LexicalEnvironment
。 - 创建了
VariableEnvironment
。
官方文档 文档将 LE 定义为:
词法环境是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。一个词法环境由一个环境记录(Environment Record)和一个对外部词法环境(outer)的可能为
null
的引用组成。
当然,除了上面所说的环境记录,父级词法环境引用之外,还包含一个 ThisBinding
,储存当前词法作用域中 this
的指向。
词法环境所要做的是跟踪变量、函数名和相关值,举个例子:
function foo() {
var a = 10
function bar() {}
}
foo()
当调用 foo
时,一个新的函数环境就生成了,这个环境可能是这样的:
ExecutionEnvironment: {
LexicalEnvironment: {
EnvironmentRecord: {
a: 10,
bar: function() {},
},
outer: <ParentLexicalEnvironment>,
ThisBinding: <globalObject>
},
...
}
LexicalEnvironment
中包含了当前执行上下文中所声明的变量,函数,除了这些环境记录外同时还会创建对它的父级执行上下文的引用 ParentLexicalEnvironment
,当 JavaScript 在当前执行上下文中找不到属性时,他们会使用 ParentLexicalEnvironment
在父级中寻找,以此类推。。。直到全局执行上下文,如果仍然没有找到则会产生 Reference Error XXX is not defined
错误。
VE(Variable Environment)变量环境
变量环境和词法环境非常相似,在 ES6 中,他们的唯一区别是:
LexicalEnvironment
用于存储函数声明和使用let
和const
所声明的变量的绑定。VariableEnvironment
存储的是使用var
声明的变量的绑定。
那如果我不用任何声明方式:
a = 1
变量 a
该存到哪里呢?
不用任何声明方式的变量将会直接视为全局对象的属性,而全局内使用 var
声明,除了会保存在当前 VariableEnvironment
中,还会映射给全局对象一份,因此在浏览器中:
var a = 1
console.log(a === window.a) // true
而 let
和 const
所定义的变量则不会映射到全局对象中。
举个例子
通过一个例子来更好地理解这些概念:
let a = 20
const b = 30
var c
function multiply(e, f) {
var g = 20
return e * f * g
}
c = multiply(20, 30)
当执行这段代码时,JavaScript 首先会创建全局执行上下文来执行全局代码,因此,在创建阶段,全局执行上下文将如下所示:
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
a: <uninitialized>,
b: <uninitialized>,
multiply: <function>
},
outer: null,
ThisBinding: <globalObject>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: undefined,
outer: null,
},
ThisBinding: <globalObject>
}
}
在执行阶段,变量赋值完成。所以全局执行上下文在执行阶段看起来像这样:
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
a: 20,
b: 30,
multiply: <function>
},
outer: null,
ThisBinding: <globalObject>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
c: undefined,
outer: null,
},
ThisBinding: <globalObject>
}
}
当执行到 multiply(20, 30)
时将会创建一个新的函数执行上下文来执行函数代码,这个函数执行上下文就像这样:
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {
0: 20,
1: 30,
length: 20
},
outer: <globalLexicalEnvironment>,
ThisBinding: <globalObject or undefined>
},
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
g: undefined,
outer: <globalLexicalEnvironment>,
ThisBinding: <globalObject or undefined>
}
}
}
在此之后,执行上下文进入执行阶段,这意味着对函数内部变量的赋值已经完成。所以函数执行上下文在执行阶段看起来像这样:
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {
0: 20,
1: 30,
length: 20
},
outer: <globalLexicalEnvironment>,
ThisBinding: <globalObject or undefined>
},
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
g: 20,
outer: <globalLexicalEnvironment>,
ThisBinding: <globalObject or undefined>
}
}
}
函数执行完成后,返回的结果被储存在变量 c
中,所以全局词法环境被更新了。之后,全局代码完成,程序结束。
Notice
在创建阶段,let
和 const
定义的变量没有任何关联的值,但是 var
定义的变量被设置为 undefined
,这是因为,在创建阶段,会扫描代码中的变量和函数声明,而函数声明完整地存储在环境中,var
声明的变量最初设置为 undefined
,let
和 const
则保持未初始化状态;这就是为什么你可以在 var
声明之前访问已定义的变量,而访问 let
和 const
定义的变量会出现引用错误。这就是我们说的变量提升(Variable Hoisting)。
闭包(Closure)
当我们执行:
var x = 1
function foo(y) {
return function (z) {
return x + y + z
}
}
var f = foo(2)
f(3)
我们在控制台中打上断点进行调试,当执行到 f(3)
的时候,控制台显示:
我们发现,当执行 f(3)
,JavaScript 创建一个新的函数执行上下文,除了包含我们上面说的属性信息之外,还多了一个 Closure
,这是因为虽然 foo
函数已经执行完毕,其执行上下文已经从执行栈中推出,但函数 f
的执行上下文由于访问了其父级 foo
执行上下文中的变量 z
,f
会产生一个 Closure
对象,保留对 z
的引用。
一旦全部代码执行完毕,f
的执行上下文将被推出,闭包也将消失。
考虑以下代码:
var x = 1
function foo(y) {
var b = 1
return function (z) {
return x + y + z
}
}
var f = foo(2)
f(3)
如果我们定义一个不会被当前上下文以外的东西所访问的变量 b
,那么 Closure
对象中会包含 b
的引用么?
按道理来说,函数 foo
的执行上下文所包含的变量都应当保存在闭包中,因为它们对于所返回的匿名函数来说是可达的。
但实际情况是:如果从代码中可以明显看出有未使用的外部变量,那么就会将其删除(至少对 V8 引擎来说是如此),因此我们可以确定:只有活动变量(也就是会被当前上下文之外的地方所访问的变量)才会被保存在闭包中。