闭包(Closure)
闭包的定义
MDN
- 一个函数和对器周围状态(lexical environment, 词法环境(作用域))的引用捆绑在一起(或者说函数被引用包围),这样的组合就是 闭包(Closure)
- 也就是说,闭包可以让一个内层函数访问到其外层函数的作用域。
- 在 js 中,每当创建一个函数,闭包将会在函数创建的同时被创建出来。
维基百科
- 闭包(Closure), 又称词法闭包(Lexical Closure)或函数闭包(function closures);
- 是支持头等函数的编程语言中,实现词法绑定的一种技术;
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
- 闭包跟函数的最大区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时确定,这样即使脱离了捕捉时的上下文,它也能照常使用;
闭包的理解
- 一个普通的函数funciton,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包。
- 从广义的角度来说:JS 中的函数都是闭包。
- 从侠义的角度来说:JS 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包。
闭包如下
function foo() {
var name = "foo";
function bar() {
console.log(name);
}
return bar;
}
var fn = foo();
fn();
上面的代码执行时的内存表现如下
执行 foo 函数时内存表现:
执行 fn(bar) 时的内存表现
可以看到当 foo 执行完成后,函数执行上下文被弹出后,其相关的AO对象应该被被销毁,但是由于bar被返回到全局中赋值给 fn ,所以 fn 指向 bar 所以bar 就不会被GC回收,因为 bar 中parent scope(编译阶段:词法解析) 指向 其父级作用域,也就是 foo函数的 AO 对象,所以foo的AO对象不会被销毁,这就是闭包形成的原因。
闭包的内存泄漏
根据上面描述的代码执行时的内存状态,可以发现本应该被销毁的 foo的 AO 对象被保存了下来。如果此时没有将 fn = null
那么这个 foo 的AO对象将会一直存在。这时就发生了内存泄漏。
进行一个简单的内存泄漏测试
// 每次创建4MB的空间大小
function createFnArray() {
// var arr = [1, 1, 1, 1, 1, 1, 1, 1,1, 1,1, 1,1 ]
// 占据的空间是4M x 100 + 其他的内存 = 400M+
// 1 -> number -> 8byte -> 8M
// js: 10 3.14 -> number -> 8byte ? js引擎
// 8byte => 2的64次方 => 4byte
// 小的数字类型, 在v8中成为Sim, 小数字 2的32次方
var arr = new Array(1024 * 1024).fill(1);
return function () {
console.log(arr.length);
};
}
// var arrayFn = createFnArray()
// arrayFn = null
// 100 * 100 = 10000 = 10s
var arrayFns = [];
for (var i = 0; i < 100; i++) {
setTimeout(() => {
arrayFns.push(createFnArray());
}, i * 100);
}
// arrayFns = null
setTimeout(() => {
for (var i = 0; i < 50; i++) {
setTimeout(() => {
arrayFns.pop();
}, 100 * i);
}
}, 10000);
AO 不使用的属性
function foo() {
var name = "why";
var age = 18;
function bar() {
console.log(name);
}
return bar;
}
var fn = foo();
fn();
- 这里的 age 记录值并没有被bar函数引用。又因 bar的parent Scope 指向 foo 的 AO 对象不会被销毁,所以按标准来说AO整个对象内容都不会被销毁。但是 js 引擎实现相当灵活, 当它发现foo的AO对象中有的记录值不会被使用了,就会将其删除掉释放内存。