不同人站在不同的角度对闭包有不同的解释,分别有以下三种比较权威的解释:
函数与其词法环境的引用共同构成了闭包。也就是说,闭包可以让你从内部函数访问外部函数作用域,在JavaScript中函数每次创建时生成闭包。——MDN
函数可以记住并访问所有的词法作用域时,就产生了闭包。即使函数是在当前作用域外执行。——《你不知道的JavaScript()上卷》
有权访问另一个函数作用域中的变量的函数就是闭包。——《JavaScript高级程序设计(第三版本)》
我们可以简单的理解为函数嵌套函数,子函数访问父函数的变量就产生了一个闭包的环境。
从上面的定义来看一个函数成为闭包具有三个特点:访问所在作用域;函数嵌套,延长内部变量的生命周期;在所在作用域外被调用。
在这里我们首先要了解什么是垃圾回收机制,什么又是变量生命周期?
生命周期可以看成是变量或者函数在相应作用域里存在的时间。变量和人一样都会有生命周期。从定义到不在使用就是一个变量的生命周期。一般来说局部变量的生命周期在函数执行完毕后就会结束,但是闭包除外。全局变量声明出来后会被全局所使用,它在页面关闭后才结束。如果变量的生拿周期已经到头那这个变量就会被垃圾回收机制抽回收然后释放它的内存。
垃圾回收机制:变量是存在内存当中的,变量在使用完没有在其它地方再使用就会被清除,用来释放内存,垃圾回收机制会按固定的时间周期性的执行。
例子:
<script>
function fn1() {
var b = 12;
}
fn1();
console.log(b) // b is not defined
</script>
为什么会报错,因为变量b在函数fn1执行完后,它在其它地方没有再使用,被垃圾回收机制回收了那就说明已经没了变量b了,所以下面的打印会报错,显示b is not defined.
例子:
<script>
function fn2() {
var n = 10;
function fn3() {
console.log(n); //10
}
fn3();
}
fn2();
</script>
从上面的例子中我们可以看出fn3成为了闭包,它符合函数成为闭包的三个特点。从中我们可以看出在函数fn2执行完后,闭包使得js的垃圾回收机制不会回收fn2所占用的资源,因为fn2内部函数fn3的执行需要依赖fn2中的变量。
所以我们可以总结出闭包的好处:在内存中维持一个变量,可以做缓存,保护函数内部的变量安全,实现封装,防止变量流入其它环境发生冲突,匿名自执行函数可以减少内逍消耗。
闭包使用时需要注意的地方:由于闭包会让函数中的变量都被保存,内存的消耗很大,严重时会造成页面卡顿。所以在退出函数前要将局部变量全部删除;闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时要小心,不要随便改变父函数内部变量的值。
循环和闭包用下面的代码来分析
<script>
function createFn() {
var result = new Array();
for (var i = 0; i < 5; i++) {
result[i] = function () {
return i;
}
}
return result;
}
</script>
这个函数会返回一个函数数组,表面上看,每个函数都应该返回自己的索引值,以此类推。但实际上每个函数都返回10.因为每个函数的作用域链中都保存着createFn()函数的活动对象,所以它们引用的都是同一个变量i.当createFn()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一变量对象,所以在每个函数内部i的值都是10.与我们想达到的效果不符。此时我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,代码如下:
<script>
function createFn() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function (num) {
return function () {
return num;
};
}(i);
}
return result
}
</script>
在上面的代码中,定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组,这里的匿名函数有一个参数num,也就是最终的函数要返回的值,在调用每个匿名函数时,传入了变量i由于函数参数是按值传递的,所以就会将变量i的当前值复制给参数num.而在这个匿名函数内部,又创建并返回了一个访问num的闭包,这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了。