代码执行过程中,执行上下文和执行上下文栈的变化过程

执行上下文和执行上下文栈变化过程

1
2
3
4
5
6
7
8
9
10
var scope = 'global scope'
function checkScope () {
var scope = 'local scope'
function f () {
return scope
}
return f()
}

checkScope()
  1. 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈中:
1
2
3
ECStack = [
globalContext,
]
  1. 全局上下文初始化:
1
2
3
4
5
globalContext = {
VO: [global, scope, checkScope], // global为全局变量
Scope: [globalContext.VO],
this: globalContext.VO,
}
  1. 初始化的同时,checkScope函数被创建,保存作用域链到函数内部属性[[scope]]:
1
checkScope.[[scope]] = [globalContext.VO]
  1. 执行checkScope函数,创建checkScope函数执行上下文(所以是在函数执行的时候创建函数执行上下文的),checkScope函数执行上下文被压入执行上下文栈。在这个阶段,执行上下文会分别创建变量对象,复制函数[[scope]]属性建立作用域链,以及确定this的指向。
1
2
3
4
ECStack = [
checkScopeContext,
globalContext,
]
  1. checkScope函数执行上下文代码执行阶段,这个时候会完成变量赋值,函数引用,以及执行其他代码:
1
2
3
4
5
6
7
8
9
10
11
checkScopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f () {},
},
Scope: [AO, globalContext.VO],
this: undefined,
}

同时,f函数被创建,保存作用域链到f函数的内部属性[[scope]]。

  1. 执行f函数,创建f函数执行上下文,f函数执行上下文被压入执行上下文栈:
1
2
3
4
5
ECStack = [
fContext,
checkScopeContext,
globalContext,
]
  1. f函数执行,沿作用域链查找scope值,返回scope值;

  2. f函数执行完毕,f函数执行上下文从执行上下文栈中弹出;

1
2
3
4
ECStack = [
checkScopeContext,
globalContext,
]
  1. checkScope函数执行完毕,checkScope函数从执行上下文栈中弹出:
1
2
3
ECStack = [
globalContext,
]

从作用域链的角度理解闭包

以前谈起闭包,只记得一句“外部函数执行完毕后,内部函数仍可访问到外部函数的变量”。如今可以从作用域链的原理上来理解。

闭包的定义

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存了起来;
  2. 从实践角度,以下函数才算是闭包:
  3. 即使创建它的上下文已经被销毁,它仍然存在(比如,内部函数从父函数中返回);
  4. 在代码中引用了自由变量(自由变量是指在函数中使用的,既不是函数参数也不是函数的局部变量的变量)。

简单例子

1
2
3
4
5
6
7
8
9
10
var fns = []
for (var i = 0; i < 3; i ++) {
fns.push(function () {
console.log(i)
})
}

fns[0]()
fns[1]()
fns[2]()

输出全部为3。
当执行到fns[0]时,全局上下文的VO为:

1
2
3
4
5
6
globalContext: {
VO: {
fns: [...],
i: 3,
}
}

fns[0]的作用域链为:

1
2
3
fns[0]Context: {
Scope: [AO, globalContext.VO],
}

fns[0]Context的AO并没有i值,沿作用域链在globalContext.VO中查找,i为3,所以打印结果为3。
fns[1]与fns[2]同理。

改成闭包:

1
2
3
4
5
6
7
8
9
10
11
12
var fns = []
for (var i = 0; i < 3; i ++) {
fns.push((function (i) {
return function () {
console.log(i)
}
})(i))
}

fns[0]()
fns[1]()
fns[2]()

输出结果为0、1、2。

fns[0]的作用域链为:

1
2
3
fns[0]Context: {
Scope: [AO, 立即执行匿名函数.AO, globalContext.VO],
}

fns[0]Context的AO没有i值,沿作用域链在立即执行匿名函数的AO中寻找,这是找到i为0,不再继续向下寻找。

1
2
3
4
5
6
7
8
9
10
11
12
var fns = []
for (var i = 0; i < 3; i ++) {
(function (i) {
fns.push(function () {
console.log(i)
})
})(i)
}

fns[0]()
fns[1]()
fns[2]()

这个例子同理。函数只要在父级,具体的位置并没有关系。

参考文献