闭包
闭包(closure)是javascript的一大难点,也是它的特色。很多高级应用都要依靠闭包来实现。
变量作用域
要理解闭包,首先要理解javascript的特殊的变量作用域。
变量的作用域无非就两种:全局变量和局部变量。
javascript语言的特别之处就在于: 函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
注意点:在函数内部声明变量的时候,一定要使用var命令。 如果不用的话,你实际上声明的是一个全局变量!
如何从外部读取函数内部的局部变量
出于种种原因,我们有时候需要获取到函数内部的局部变量。但是,上面已经说过了,正常情况下,这是办不到的!只有通过变通的方法才能实现。
那就是在函数内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。
这就是Javascript语言特有的 "链式作用域"结构(chain scope), 子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
闭包的概念
上面代码中的f2函数,就是闭包。
各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数。
由于在javascript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。
所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包的用途
1、函数作为返回值,2、函数作为参数传递。
- 函数作为返回值
function fn() {
var max = 10;
return function bar(x) {
if(x > max){
console.log(x)
}
}
}
如上代码,bar函数作为返回值,赋值给f1变量。执行f1(15)时,用到了fn作用域下的max变量的值。
- 函数作为参数被传递
var max = 10;
var fn = function(x) {
if(x > max){
console.log(x)
}
}
(function(f) {
var max = 10;
f(15)
})(fn)
如上代码中,fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
补充:JS中的函数定义
JS中定义一个函数,最常用的就是函数声明和函数表达式
Js中的函数声明是指下面的形式:
function functionName(){
}
函数表达式则是类似表达式那样来声明一个函数:
var functionName = function(){
}
我们可以使用函数表达式创建一个函数并马上执行它,如:
(function() {
var a, b // local variables
// ... // and the code
})()
()();第一个括号里放一个无名的函数。
二者区别:js的解析器对函数声明与函数表达式并不是一视同仁地对待的。对于函数声明,js解析器会优先读取,确保在所有代码执行之前声明已经被解析,而函数表达式,如同定义其它基本类型的变量一样,只在执行到某一句时也会对其进行解析,所以在实际中,它们还是会有差异的,具体表现在,当使用函数声明的形式来定义函数时,可将调用语句写在函数声明之前,而后者,这样做的话会报错。
闭包的好处
1、模块化代码
使用自执行的匿名函数来模拟块级作用域
(function(){
// 这里为块级作用域
})();
该方法经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数影响全局作用域。也可以减少如闭包这样的对内存的占用,由于匿名函数没有变量指向,执行完毕就可以立即销毁其作用域链。
var test = (function(){
var a= 1;
return function(){
a++;
alert(a);
}
})();
test();//2
test();//3
实现a的自加,不污染全局。
2、循环闭包
循环给每个li注册一个click事件,点击alert序号。代码如下:
var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
aLi[i].onclick = function(){
alert( i );//all are aLi.length!
}
}
}
点击后会一直弹出同一个值 aLi.length 而不是123。当点击之前,循环已经结束,i值为aLi.length。
利用闭包,建一个匿名函数,将每个i存在内存中, onclick函数用的时候提取出外部匿名函数的i值。代码如下:
var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
(function(i){
aLi[i].onclick = function(){
alert( i );
}
})(i);
}
}
或者:
function showAllNum( aLi ){
for( var i =0,len = aLi.length ;i<len;i++ ){
aLi[i].onclick = (function(i){
return function(){
alert( i );
}
})(i);
}
}
解释:实际上只是通过函数的赋值表式方式付给了标签点击事件,并没有运行;当遍历完后,i变成标签组的长度,根据作用域的原理,向上找到for函数里的i,所以点击执行的时候都会弹出标签组的长度。闭包可以使变量长期驻扎在内存当中,我们在绑定事件的时候让它自执行一次,把每一次的变量存到内存中;点击执行的时候就会弹出对应本作用域i的序号。
闭包的作用
-
可以减少全局变量的对象,防止全局变量过去庞大,导致难以维护
-
防止可修改变量,因为内部的变量外部是无法访问的,并且也不可修改的。安全
-
可以读取函数内部的变量,可以把变量始终保存在内存中,不会在外层函数调用后被自动清除
闭包的优点
-
变量长期驻扎在内存中
-
避免全局变量的污染
-
私有成员的存在
闭包的特性
-
函数套函数
-
内部函数可以直接使用外部函数的局部变量或参数
-
变量或参数不会被垃圾回收机制回收GC
闭包的缺点
常驻内存,会增大内存的使用量,使用不当会造成内存泄露,详解:
1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象object使用,把闭包当作它的公用方法Public Method,把内部变量当作它的私有属性private value,这时一定要小心,不要随便改变父函数内部变量的值。
闭包在IE下内存泄露问题
IE9之前,JScript对象和COM对象使用不同的垃圾收集例程,那么闭包会引起一些问题。
创建一个闭包,而后闭包有创建一个循环引用,那么该元素将无法销毁。常见的就是dom获取的元素或数组的属性(或方法)再去调用自己属性等。例如:
function handler(){
var ele = document.getElementById("ele");
ele.onclick = function(){
alert(ele.id);
}
}
闭包会引用包含函数的整个活动对象,即是闭包不直接引用ele,活动对象依然会对其保存一个引用,那么设置null就可以断开保存的引用,释放内存。代码如下:
function handler(){
var ele = document.getElementById("ele");
var id = ele.id;
ele.onclick = function(){
alert(id);
}
ele = null;
}
闭包的工作原理
因为闭包只有在被调用时才执行操作,所以它可以被用来定义控制结构。多个函数可以使用同一个环境,这使得他们可以通过改变那个环境相互交流。