当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
—— 你不知道的JavaScript(上卷)
一、闭包的定义
1、闭包的构成
首先,闭包由两部分构成:函数、创建该函数的环境,环境由闭包创建时在作用域中的任何局部变量组成。
JS的变量作用域
变量的作用域有两种:全局变量和局部变量。
函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
那么,如何从外部读取函数内部的局部变量?在下面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。
这就是Javascript语言特有的 链式作用域 结构(chain scope):子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,就可以在f1外部读取f1的内部变量。
function f1(){
var n=999;
function f2(){
alert(n);
}
}
var test = f1();// 外部函数调用之后其变量对象n本应该被销毁
test();// alert 999,但闭包阻止了它们的销毁,所以仍然可以访问外部函数的变量对象
上面代码中的f2函数,就是闭包。
在JS中,只有函数内部的成员才能读取局部变量。所以在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
二、闭包的特性
在JavaScript中,外部函数调用之后其变量对象本应该被销毁,但闭包阻止了它们的销毁,所以仍然可以访问外部函数的变量对象。
function addCalculator (x) {
return function (y) {
return x + y;
}
}
var add1 = addCalculator(1);
console.log(add1(1)); //2
add1 = null;// 释放对闭包的引用
console.log(add1(1)); //Uncaught TypeError: add1 is not a function
通常情况下,函数的作用域及其所有变量都会在函数执行结束后被销毁。
但如果创建了一个闭包的话,这个函数的作用域就会一直保存到闭包不存在为止。
闭包的特性一般为以下几点:
1、闭包一般为外部函数嵌套的内部函数、以及创建该内部函数的环境组成的;
2、闭包可以引用外部函数的参数和变量,即可以访问外部的环境;
3、闭包未被释放回收的情况下,若闭包中引用了外部函数的参数和变量,即使外部环境已经被释放回收,但是外部函数的参数和变量依然不会被垃圾回收机制回收。
三、闭包的作用
1、通过闭包来模拟私有方法
私有方法有利于限制对代码的访问,而且可以避免非核心的方法干扰代码的公共接口,减少全局污染。
下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式(module pattern)
var calculator = (function(){
var a = 1;
function addCalculator(val){
a += val
}
return {
add1:function() {
addCalculator(1);
},
add2:function() {
addCalculator(2);
},
result:function() {
return a
}
}
})();
console.log(calculator.result()); // 1
calculator.add1();
console.log(calculator.result()); // 2
calculator.add2();
console.log(calculator.result()); // 4
在之前的示例中,每个闭包都有它自己的词法环境。而这次例子中只创建了一个词法环境,为三个函数所共享:calculator.add1,calculator.add2和 calculator.result。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 a的变量和名为 addCalculator的函数,这两项都无法在这个匿名函数外部直接访问,必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包,它们都可以访问 a变量和 addCalculator函数。
四、闭包的优缺点
1、优点
- 保护函数内的变量安全 ,实现封装,防止变量流入其他环境发生命名冲突
- 在内存中维持一个变量,可以做缓存(但使用多了同时也是一项缺点,消耗内存)
- 匿名立即执行函数可以减少内存消耗
2、缺点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以滥用闭包的话会造成网页的性能问题,在IE(IE9)之前可能导致内存泄露。
解决方法:在退出函数之前,将不使用的局部变量全部删除(例如手动赋值为null) - 由于闭包涉及跨域访问,所以会导致性能损失。
解决方法:可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。
引申:为何闭包的不当使用会在IE(IE9)之前可能导致内存泄漏,即无法回收变量的问题?
因为IE(IE9)之前的JavaScript引擎使用的垃圾回收算法是引用计数法,对于循环引用将会导致GC(Garbage Collection)无法回收垃圾。
注:在后面的章节中,我会找机会继续讨论什么是GC(Garbage Collection)
PS:更新来啦O(∩_∩)O,下一篇文章讲到了GC(Garbage Collection):JS的垃圾回收机制
网友评论