前言
为了能够真正的了解浏览器解析闭包的过程,各种查资料,看代码,最后将整理好的干货分享出来,希望不喜轻喷。
通过这篇文章你能了解什么?
1、一些与闭包相关的基本概念
2、浏览器编译器和预编译器如何解析闭包
3、程序在运行时闭包如何变化
作用域
可访问变量的集合。
作用域链
作用域是分层的,为了能让局部作用域访问全局作用域的变量,浏览器通过一种链式结构将作用域串联起来,我们将这种结构称之为作用域链。
闭包
一个函数和其周围的环境绑定在一起,这种组合称之为闭包。
v8生成闭包的全过程
var a = 2;
function Fn(){
var b = 1;
var c = 3;
function Fn1(){
var d = 4;
function Fn2(){
console.log('b', b);
function Fn3(){
console.log('d', d);
}
console.dir(Fn3);
}
console.dir(Fn2);
Fn2();
}
console.dir(Fn1);
Fn1();
}
Fn();
以上面的代码为例,执行详情如下:
1、首先生成全局执行上下文,压入执行环境栈;
2、编译阶段,创建变量名为Fn的变量,指向函数Fn的推地址;
3、启用预编译器,对函数Fn进行预编译,初始化Fn的变量对象VO(Fn)和[[Scopes]] = [Global],预编译继续;
4、对函数Fn1进行预编译,初始化Fn1的变量对象VO(Fn1)和[[Scopes]] = [VO(Fn), Global],
扫描发现没有对外部变量的引用,预编译继续;
5、对函数Fn2进行预编译,初始化Fn2的变量对象VO(Fn2)和[[Scopes]] = [VO(Fn1), VO(Fn), Global],
扫描发现有对外部变量的引用,通过[[Scopes]]找到外部变量属于VO(Fn1),生成闭包对象Closure(Fn1),将
引用的变量复制到闭包里,替换掉[[Scopes]]中的VO(Fn1),即[[Scopes]] = [Closure(Fn1), VO(Fn), Global],
预编译继续;
6、对函数Fn3进行预编译,初始化Fn3的变量对象VO(Fn3)和[[Scopes]] = [VO(Fn2), Closure(Fn1), VO(Fn), Global],扫描发现有对外部变量的引用,通过[[Scopes]]找到外部变量属于VO(Fn),生成闭包对象Closure(Fn),将
引用变量复制到闭包里,替换掉[[Scopes]]中的VO(Fn),即[[Scopes]] = [VO(Fn2), Closure(Fn1), Closure(Fn), Global],清除掉VO,[[Scopes]] = [Closure(Fn1), Closure(Fn), Global],
即预编结束;
7、执行函数Fn,创建Fn执行上下文,压入执行环境栈,激活AO(Fn),初始化作用域链[AO(Fn), Global],
执行代码b = 1, c = 3, console.dir(Fn1), Fn1();
8、执行函数Fn1,创建Fn1执行上下文,压入执行环境栈,激活AO(Fn1),初始化作用域链
[AO(Fn1), Closure(Fn), Global],执行代码d = 4, console.dir(Fn2), Fn2();
9、执行函数Fn2,创建Fn2执行上下文,压入执行环境栈,激活AO(Fn2),初始化作用域链
[AO(Fn2), Closure(Fn1), Closure(Fn), Global],执行代码console.log('d', d), console.dir(Fn3);
10、Fn2执行完毕,Fn2执行上下文出栈,释放AO(Fn2),释放作用域链;
11、Fn1执行完毕,Fn1执行上下文出栈,释放AO(Fn1),释放作用域链;
12、Fn执行完毕,Fn执行上下文出栈,释放AO(Fn),释放作用域链;
13、执行结束。
备注
1、函数Fn、Fn1、Fn2和Fn3的[[Scopes]]共享最终的[[Scopes]] = [Closure(Fn1), Closure(Fn), Global],Fn的[[Scopes]] = [Global],Fn1的[[Scope]] = [VO(Fn), Global] = [Closure(Fn), Global],Fn2的[[Scopes]] = [Closure(Fn1), VO(Fn), Global] = [Closure(Fn1), Closure(Fn), Global],Fn3的[[Scopes]] = [Closure(Fn1), Closure(Fn), Global]。
2、VO是一个变量对象,函数声明的变量集合,是在函数编译时生成的。AO是一个活动对象,包括this、arguments和变量集合,是在函数执行时生成的。
理论联系实践
验证1
var a = 2;
function Fn(){
var b = 1;
}
console.dir(Fn);
验证2
var a = 2;
function Fn(){
var b = 1;
console.log('a', a);
}
console.dir(Fn);
验证3
var a = 2;
function Fn(){
var b = 1;
console.log('b', b);
}
console.dir(Fn);
注意:验证1-3是为了表明Fn在没有函数嵌套时的状态。
验证4
var a = 2;
function Fn(){
var b = 1;
function Fn1(){
console.log('a', a);
}
console.dir(Fn1);
}
console.dir(Fn);
验证5
var a = 2;
function Fn(){
var b = 1;
function Fn1(){
console.log('b', b);
}
console.dir(Fn1);
}
console.dir(Fn);
注意:验证4-5是为了表明Fn在有函数嵌套时的状态。
验证6
var a = 2;
function Fn(){
var b = 1;
function Fn1(){
console.log('a', a);
}
console.dir(Fn1);
}
Fn();
验证7
var a = 2;
function Fn(){
var b = 1;
function Fn1(){
console.log('b', b);
}
console.dir(Fn1);
}
Fn();
根据验证6和验证7的结果,以及和前面验证1-5函数Fn的状态对比,我们得出以下结论:
结论1:子函数引用全局变量不会生成闭包;
结论2:子函数引用父函数变量会生成闭包;
结论3:闭包的形成和是否return函数没有关系。
新问题:子函数引用祖先函数的变量会生成闭包吗?让我们继续往下验证。
验证8
var a = 2;
function Fn(){
var b = 1;
function Fn1(){
console.log('a', a);
function Fn2(){
console.log('b', b);
}
console.dir(Fn2);
}
console.dir(Fn1);
Fn1();
}
Fn();
到这里,我们回答了上面的问题:
扩展结论2:子函数引用父/祖先函数的变量会生成闭包;
验证9
var a = 2;
function Fn(){
var b = 1;
var c = 3;
function Fn1(){
console.log('c', c);
function Fn2(){
console.log('b', b);
}
console.dir(Fn2);
}
console.dir(Fn1);
Fn1();
}
Fn();
结合验证8和验证9,确认了结论1;
新问题:如果生成多个闭包,闭包的名字、数量和顺序是怎么样的?
验证10
var a = 2;
function Fn(){
var b = 1;
var c = 3;
function Fn1(){
var d = 4;
console.log('c', c);
function Fn2(){
console.log('b and d', b, d);
}
console.dir(Fn2);
}
console.dir(Fn1);
Fn1();
}
Fn();
验证11
var a = 2;
function Fn(){
var b = 1;
var c = 3;
function Fn1(){
var d = 4;
function Fn2(){
console.log('b', b);
function Fn3(){
console.log('d', d);
}
console.dir(Fn3);
}
console.dir(Fn2);
Fn2();
}
console.dir(Fn1);
Fn1();
}
Fn();
验证12
var a = 2;
function Fn(){
var b = 1;
var c = 3;
function Fn1(){
var d = 4;
function Fn2(){
console.log('d', d);
function Fn3(){
console.log('b', b);
}
console.dir(Fn3);
}
console.dir(Fn2);
Fn2();
}
console.dir(Fn1);
Fn1();
}
Fn();
结论4:闭包的名字默认是被引用变量的函数名;
结论5:闭包的数量等于子函数引用的变量所属函数的数量;
结论6:闭包的顺序为被引用的变量所属函数在函数嵌套中由父/祖先函数向子函数的顺序;