闭包的设计目的是为了存储私有变量,延长变量的生命周期,只有特定的接口才能访问该私有变量,可以防止防止全局变量命名冲突。
闭包常见例子:
function fn() {
var a = 1;
function foo1() {
console.log(a++);
}
return foo1;
}
var foo2 = fn();
foo2();
foo2();
这个闭包包括了javascript的三个特性:
1、父元素嵌套了子元素
2、子元素里引用了父元素里的变量
3、函数是一等公民,可以作为返回值返回给变量
现在看看V8引擎是如何实现这个闭包这个技术的
1、创建全局上下文,函数fn被创建,V8将函数声明转换为函数对象,保存作用域链到[[scope]]属性中,将代码转换成字符串保存到code属性中
fn.[[scope]]={
globalContext.VO;
}
2、创建函数执行上下文,fn函数执行上下文被压入执行上下文栈顶端
ECStack = [
fnContext,
globalContext
];
3、进行预解析,赋值函数对象中的[[scope]]属性值创建作用域链
fnContext = {
Scope: fn.[[scope]],
}
4、用arguement创建活动对象,随后初始化活动对象,加入形参,变量提升,函数提升
fnContext = {
AO: {
arguments: {
length: 0
},
a: undefined
},
Scope: [AO, [[Scope]]]
}
此时如果碰到了函数,V8不会直接跳过,而是会快速地解析内部代码,判断是否使用了外部变量,如果使用了,那么会复制一份保存到堆内存中其名为closure(闭包),并将closure保存到子函数的[[scope]]属性中,该赋值操作会在父函数执行上下文销毁之前进行。在这个例子中,foo1使用了fn中的变量,会将其保存到[[scope]]中,可以在foo1的prototype中看到。
fnContext = {
AO: {
arguments: {
length: 0
},
a: 1
},
Scope: [AO, [[Scope]]]
}
6、最后返回了foo1的指针给全局变量foo2,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
根据垃圾回收规则,没有被引用的对象会被回收,而fn函数最后把闭包函数对象的指针返回给了全局变量foo2,虽然foo1的父元素执行上下文最后出栈,foo1中指向闭包函数对象的指针也被销毁,但是全局变量依然保存了,导致闭包函数对象一直保留在内存中,而Closure对象一直保存在[[scope]]中,所以不会被清除,下次再次调用foo2时可以直接从堆内存中取出a。
function fn() {
var a = 1;
function foo1() {
console.log(a++);
}
return foo1;
}
var foo2 = fn();
foo2();
foo2();
function fn() {
var a = 1;
function foo1() {
console.log(a++);
}
return foo1;
}
fn()();
fn()();
如果返回给全局变量可以实现叠加器的效果,因为每次调用的都是指向同一个对象
而如果不返回,那么闭包函数对象的生命周期到父函数执行上下文销毁就结束了,会被回收,那么对象中的闭包对象也就不复存在了,再次调用的话,就是重新分配内存空间。
function sum(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
var sum1 = sum(1)(2)(3);
执行结果为
看看function©这个匿名闭包函数的[[scope]]
function sum(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
var sum1 = sum(1)(2);
console.log(sum1.prototype);