本文主要记录
全局执行上下文
和函数执行上下文
1. 执行上下文
简而言之, 执行上下文
就是 JavaScript
代码运行的环境。
1.1 分类
在JavaScript中,有三种执行上下文类型:
-
全局执行上下文 :这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个
全局的 window对象
(浏览器的情况下),并且设置 this 的值等于这个全局对象
。一个程序中只会有一个全局执行上下文。 -
函数执行上下文 : 每当一个函数
被调用时
,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的
。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。 -
Eval
函数执行上下文 : 行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。
1.2 执行上下文存储的内容
在不同的执行上下文中所存储的内容不尽相同,但是每一个执行上下文,都会存在一个 变量对象(Variable Object
简称VO
), 这个变量对象将会存储当前执行函数的变量声明等内容。
2. 执行栈
-
执行栈(
Execution Context Stack
简称ECS
), 也就是在其它编程语言中所说的“调用栈
”,是一种拥有LIFO
(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。 -
当
JavaScript
引擎第一次遇到你的脚本时,它会创建一个全局执行上下文
并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。 -
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
3. 全局执行上下文
所有的 JavaScript
代码在执行之前就会创建一个执行栈,而每一个执行栈的最开始的时候,都会存放一个 全局执行上下文(Global Execution Context
简称GEC
)。 因为执行栈是一个栈结构,那么则表示 GEC
(全局执行上下文) 会存在于栈底,当全部的 JavaScript
代码执行完毕之后,GEC
才会出栈。
3.1 GEC
(全局执行上下文) 中存储的内容
- 全局对象(
Global Object
简称GO
): 这里面存储的是在全局中声明的所有变量声明和函数声明。但是在GO
(全局对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
- 变量声明: 在
GO
中存储的变量声明,在声明的时候,全部声明为undefined
,在真正执行的时候,才会给每一个变量进行赋值操作。 - 函数声明: 在
GO
中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中。
- 执行代码: 就是在全局中将要执行的所有代码。
3.2 window
下的全局执行上下文
在浏览器中执行 JavaScript
代码(或者说是 V8
引擎下执行 JavaScript
代码) 的时候,GEC
(全局执行上下文) 中的 VO
(变量对象),会被赋值为 GO
(全局对象),这个时候 VO
(变量对象)和 GO
(全局对象)是同一个东西,在浏览器下就是 window
对象。所以在浏览器执行的 JavaScript
代码的时候,在全局中打印 this
的话,打印出来的是 window
对象。
var foo = 'hello world';
function bar() {
console.log(name);
}
bar();
因为在浏览器中的 window
对象中,window
对象本身就绑定了很多属性,比如定时器、一些浏览器事件函数等等。但是最神奇的一点是在 window
对象中,又绑定了 window
,如此循环。如果打印一下console.log(window.window.window....)
,并不会报错,而是继续打印出 window
对象。这是因为在 window
这个对象中,又绑定了这个 GO
(全局对象),因为浏览器中的全局对象是 window
,所以如此往复,window
中嵌套 window
。
4. 函数执行上下文(FEC
)
在整个 ECS
(执行栈)中,除了最底层的 GEC
(全局执行上下文),剩下的其他执行上下文就是 函数执行上下文(Function Execution Context
简称FEC
)。
其实还有
Eval
函数执行上下文, 本文不讲述Eval
函数执行上下文。
JavaScript
代码中的每一个函数都会创建一个 FEC
(函数执行上下文),只有在执行到该函数的时候,才会将其 FEC
(函数执行上下文)存入执行栈中,当执行完后又会将该 FEC
(函数执行上下文)进行销毁,即出栈。
4.1 FEC
中存储的内容
在 FEC
与 GEC
存储的内容不尽相同:
- 活动对象(
Activation Object
简称AO
): 这里面存储的是在全局中声明的所有变量声明和函数声明。但是在AO
(活动对象) 中存储的变量声明与函数声明的存储形式,又是截然不同:
- 变量声明: 在
AO
中存储的变量声明,在声明的时候,全部声明为undefined
,在真正执行的时候,才会给每一个变量进行赋值操作。 - 函数声明: 在
AO
中存储的函数声明,在声明的时候,就已经为函数创造了其内存空间, 在函数声明中引入的则是该内存空间的地址,该函数的内部代码将会存储在一个独立的内存空间当中。
- 执行代码: 就是在全局中将要执行的所有代码。
重点:
- 当
GEC
在浏览器下运行的时候,其VO
(变量对象)会被赋值为GO
(全局对象)。- 在
FEC
(函数执行上下文)中的VO
(变量对象),会被赋值为AO
(活动对象)。
执行下面的代码,我将该代码在浏览器中运行时整个 ECS
画个草图展示一下:
var foo = 'hello world';
function bar() {
var age = 10;
console.log(name);
}
bar();
5. 经典案例
比较下面两段代码,试述两段代码的不同之处。
// 第一个
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
// 第二个
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
答案:两段代码执行结果都是 local scope
。但是这两个代码在执行栈的执行过程则是截然不同。
5.1 代码一的解析
- 首先创建一个
GEC
(全局执行上下文),压入到ECS
(执行栈)的栈底;将全局中的 变量声明 和 函数声明 都绑定到GO
(全局对象)上。
- 开始执行
GEC
,给变量scope
赋值global scope
;当执行到checkscope()
调用checkscope
的函数的时候,会给checkscope
函数创建一个函数执行上下文(FEC
),将其压入ECS
(执行栈)。
2. 执行 checkscope
函数 FEC
(函数执行上下文)中的代码,给 变量 scope
赋值 local scope
;当执行到 return f()
的时候,因为又调用了函数 f
,所以又要创建一个函数 f
的执行上下文,压入到 ECS
(执行栈)中。
- 当函数
f
全部执行完毕之后,那么他的执行上下文将会出栈。
- 当函数
checkscope
全部执行完毕之后,他的执行上下文将会出栈。
- 当
ECS
(执行栈)中只剩下GEC
(全局执行上下文),没有任何其他的代码需要去执行的时候,GEC
(全局执行上下文)也会出栈,将ECS
(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。
5.2 代码二的解析
- 首先创建一个
GEC
(全局执行上下文),压入ECS
(执行栈)的栈底;将全局中所有变量声明
和函数声明
都绑定在GO
(全局对象)中。
注意:在代码执行中我写的是执行
checkscope()
,并不是执行checkscope()()
,因为执行函数是只有一个小括号才表示执行;第二个小括号表示的是,执行checkscope()
后返回的函数再去执行。
- 开始执行
GEC
(全局执行上下文),给 变量scope
赋值global scope
;当执行到checkscope()
调用checkscope
函数的时候,会给checkscope
函数创建一个FEC
(函数执行上下文),压入ECS
(执行栈)中。
注意:红色圈起来的部分,代码中只是将 f
这个函数返回了,并没有去执行,所以下一步并不会给 f
函数创建一个执行上下文。
- 经过第二步之后,
checkscope
所有的代码都已经执行完毕,注意return
返回的是f
函数,并不是f()
,只有函数调用的时候才会创建FEC
(函数执行上下文)。所以这里将checkscope
函数执行完毕之后,就会将checkscope
的执行上下文销毁。
- 这时候,
checkscope
函数最后返回的函数f
,会被调用 —— 因为checkscope()() => f()
。这里才对f
函数进行了调用,所以又会创建一个函数f
的执行上下文。
- 函数
f
执行完毕之后,就会将f
的执行上下文进行销毁。
- 当
ECS
(执行栈)中只剩下GEC
(全局执行上下文),没有任何其他的代码需要去执行的时候,GEC
(全局执行上下文)也会出栈,将ECS
(执行栈)清空。当再次执行代码的时候,继续重复上述的步骤。
The execution context, lexical environment, variable environment
6. 几道面试题
6.1 例题1
var n = 100
function foo() {
n = 200
}
foo()
console.log(n)
// 200
6.2 例题2
function foo() {
console.log(n) //undefined
var n = 200
console.log(n) //200
}
var n = 100
foo()
// undefined
// 200
6.3 例题3
var n = 100;
function foo1() {
console.log(n) //100
}
function foo2() {
var n = 200
console.log(n) //200
foo1()
}
foo2()
console.log(n) //100
// 200
// 100
// 100
6.4 例题4
var a = 100
function foo() {
console.log(a) //undefined 局部变量
return
var a = 200
}
foo()
// undefined
6.5 例题5
先说一个 bug
,代码里不要这么写。
这是JavaScript的bug
function foo() {
m = 100
}
foo()
console.log(m) //可以在外部访问到m,注意:m前没用var声明
// 100
看题:
function foo() {
var a = b = 10
// => 可以转成下面的两行代码
// var a = 10
// b = 10
}
foo()
// console.log(a) //ReferenceError: a is not defined
console.log(b) // 10