执行上下文和执行上下文栈变化过程
1 | var scope = 'global scope' |
- 执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈中:
1 | ECStack = [ |
- 全局上下文初始化:
1 | globalContext = { |
- 初始化的同时,checkScope函数被创建,保存作用域链到函数内部属性[[scope]]:
1 | checkScope.[[scope]] = [globalContext.VO] |
- 执行checkScope函数,创建checkScope函数执行上下文(所以是在函数执行的时候创建函数执行上下文的),checkScope函数执行上下文被压入执行上下文栈。在这个阶段,执行上下文会分别创建变量对象,复制函数[[scope]]属性建立作用域链,以及确定this的指向。
1 | ECStack = [ |
- checkScope函数执行上下文代码执行阶段,这个时候会完成变量赋值,函数引用,以及执行其他代码:
1 | checkScopeContext = { |
同时,f函数被创建,保存作用域链到f函数的内部属性[[scope]]。
- 执行f函数,创建f函数执行上下文,f函数执行上下文被压入执行上下文栈:
1 | ECStack = [ |
f函数执行,沿作用域链查找scope值,返回scope值;
f函数执行完毕,f函数执行上下文从执行上下文栈中弹出;
1 | ECStack = [ |
- checkScope函数执行完毕,checkScope函数从执行上下文栈中弹出:
1 | ECStack = [ |
从作用域链的角度理解闭包
以前谈起闭包,只记得一句“外部函数执行完毕后,内部函数仍可访问到外部函数的变量”。如今可以从作用域链的原理上来理解。
闭包的定义
ECMAScript中,闭包指的是:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存了起来;
- 从实践角度,以下函数才算是闭包:
- 即使创建它的上下文已经被销毁,它仍然存在(比如,内部函数从父函数中返回);
- 在代码中引用了自由变量(自由变量是指在函数中使用的,既不是函数参数也不是函数的局部变量的变量)。
简单例子
1 | var fns = [] |
输出全部为3。
当执行到fns[0]时,全局上下文的VO为:
1 | globalContext: { |
fns[0]的作用域链为:
1 | fns[0]Context: { |
fns[0]Context的AO并没有i值,沿作用域链在globalContext.VO中查找,i为3,所以打印结果为3。
fns[1]与fns[2]同理。
改成闭包:
1 | var fns = [] |
输出结果为0、1、2。
fns[0]的作用域链为:
1 | fns[0]Context: { |
fns[0]Context的AO没有i值,沿作用域链在立即执行匿名函数的AO中寻找,这是找到i为0,不再继续向下寻找。
1 | var fns = [] |
这个例子同理。函数只要在父级,具体的位置并没有关系。
参考文献
- 冴羽,JavaScript深入之执行上下文
- 冴羽,JavaScript深入之闭包
- 波同学,前端基础进阶系列