🍎 JavaScript 执行上下文与执行栈
# 变量提升与函数提升
- 变量声明提升:
- 通过
var
定义(声明)的变量,在定义语句之前就可以访问。值为undefined
。
- 通过
- 函数声明提升:
- 通过
function
声明的函数,在之前就可以直接调用,值为函数定义(对象)。但是通过var
定义的函数不适用函数提升(为变量提升)。
- 通过
🌰 例子:
var a = 3
function fn(){
// var a
console.log(a) // undefined
var a = 4
}
fn()
2
3
4
5
6
7
fn2() // undefined 可调用
function fn2(){
console.log('fn2()')
}
2
3
4
5
fn3() // error
var fn3() = function (){
console.log('fn3()')
}
2
3
4
5
❓ 变量提升和函数提升是如何产生的?
# 执行上下文
执行上下文是一种对 JavaScript 代码执行环境的抽象概念。也就是说,只要有 JavaScript 代码执行,就一定是运行在执行上下文中。
按照位置给代码进行分类:
全局代码
函数代码
Eval 函数执行上下文。运行在eval
函数中。很少使用而且不建议使用。
全局执行上下文:(只有一个)
在执行全局代码前将
window
确定为全局执行上下文。对全局数据进行预处理:
使用
var
定义的全局变量值为undefined
,添加为window
的属性;使用
function
声明的全局函数赋值,添加为window
的方法;this
赋值为window
。(在浏览器中
window
为全局对象)
开始执行全局代码。
函数执行上下文 :(存在无数个)
- 在调用函数时,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中);
- 对局部数据进行预处理:
- 形参变量赋值 **
实参
的数据 **,添加为执行上下文的属性; arguments
赋值 **实参
的数据列表(伪数组)**,添加为执行上下文的属性;- 使用
var
定义的局部变量赋值undefined
,添加为执行上下文的属性; - 使用
function
声明的函数,赋值fun
,添加为执行上下文的方法; this
赋值调用函数的对象
。
- 形参变量赋值 **
- 开始执行函数体代码。
🌰 例子:全局执行上下文:
console.log(a1) // window.a1
a2() // window.a2()
console.log(this) // window
var a1 = 1
function a2() {
console.log('a2()')
}
2
3
4
5
6
7
8
🌰 例子:函数执行上下文:
function fn(a1) {
console.log(a1) // 2
console.log(a2) // undefined
a3() // a3()
console.log(this) // window 调用fn()的对象
console.log(arguments) // 伪数组(2,3)
var a2 = 3
function a3() {
console.log('a3()')
}
}
fn(2, 3)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生命周期
执行上下文的生命周期分为三个阶段:
- 创建阶段;
- 执行阶段;
- 回收阶段;
# 创建阶段
当函数被调用,但未执行任何其内部代码之前。
完成三件事:
- 确定
this
的值(this
binding ,this
的绑定);因为this
的值是在执行的时候被确认的,定义的时候不能确认。 - 创建词法环境(Lexical Environment)
- 创建变量环境(Variable Environment)
ExecutionContext = { ThisBinding = <this value>, LexicalEnvironment = { ... }, VariableEnvironment = { ... } }
1
2
3
4
5词法环境有两个组成部分:
- 全局环境:没有外部环境的词法环境,其外部环境引用的是
null
;有一个全局对象,this
指向这个全局对象。 - 函数环境:用户在函数中定义的变量被存储在环境记录中,包含
arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。
- 全局环境:没有外部环境的词法环境,其外部环境引用的是
变量环境:也是一个词法环境,因此具有上面定义的词法环境的所有属性。
ES6 中,词法环境和变量环境的区别在于,词法环境用于存储函数声明和变量(
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);
1
2
3
4
5
6
7
8
9
10let
和const
定义的变量a
和b
在创建阶段没有被赋值,但是var
声明的变量从在创建阶段被赋值为undefined
。因为创建阶段会在代码中扫描变量和函数声明, 将函数声明存储在环境中;而变量会变初始化为undefined
(var
声明的变量)和uninitialized
(未初始化)(let
和const
声明的变量)。这是变量提升的实际原因。
- 确定
# 执行阶段
在这个阶段,执行变量赋值、代码执行。如果 JavaScript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配为 undefined
值。
# 回收阶段
执行上下文出栈,等待虚拟机回收执行上下文。
# 执行上下文栈
- 在全局代码执行之前,JavaScript 引擎就会创建一个栈来存储管理所有的执行上下文对象。
- 在全局执行上下文
window
确定后,将其添加到栈中(压栈)。 - (调用函数时)在函数执行上下文创建后,将其添加到栈中(压栈)。
- 在当前函数执行完后,将栈顶的对象移除(出栈)。
- 当所有的代码执行完后,栈中只剩下
window
。
🌰 例子:
// 1.进入全局执行上下文
var a =10
var bar = function(x) {
var b = 5
foo(x + b) // 3. 进入foo执行上下文
}
var foo = function(y) {
var c = 5
console.log(a + c + y)
}
bar(10) // 2. 进入bar函数执行上下文
2
3
4
5
6
7
8
9
10
11
12
13
14
# 面试题
1️⃣ :
- 依次输出的内容。
- 整个过程产生了几个执行上下文?
console.log('global begin:' + i)
var i = 1
foo(1)
function foo(i){
if(i == 4) {
return;
}
console.log('foo() begin:' + i)
foo(i + 1)
console.log('foo() end:' + i)
}
console.log('global end:' + i)
2
3
4
5
6
7
8
9
10
11
12
5
。
2️⃣:
function a() { }
var a
console.log(typeof a) // function
2
3
先执行变量提升再执行函数提升。
if(!(b in window)) {
var b = 1
}
console.log(b) // undefined
2
3
4
var c = 1
function c(c){
console.log(c)
}
c(2) // TypeError: c is not a function. (In 'c(2)', 'c' is 1)
2
3
4
5