test3
words • 88888
wangdazhuzhongleiyang :::info 这个高亮块 :::
hangneidaima
本文主要记录
全局执行上下文和函数执行上下文
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(全局对象)上。
2. 开始执行 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()**后返回的函数再去执行。
2. 开始执行 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