个人总结、欢迎指正
在认识闭包之前先要知道JavaScript的变量作用域,JavaScript变量的作用域分为全局变量和局部变量。
在JavaScript中,函数内部可以访问全局变量,但函数外部不能访问内部变量。
那来看看什么是闭包呢?
一、闭包(closure)的定义
各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂,借阮一峰的话就是:闭包就是能够读取其他函数内部变量的函数。
那闭包如何去创建呢?
二、创建闭包
在函数的内部,再定义一个函数,并返回,就可以在函数外部访问到函数内的局部变量,操作如下:
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
三、闭包的优缺点
1.优点
- 函数外部可以读取函数内部的变量;
- 让闭包里面变量的值始终保持在内存中。
2、缺点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除(涉及到垃圾回收机制(garbage collection))。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
四、闭包的用途
其实,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。
1.匿名自执行函数
我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。
除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:
var datamodel = {
table : [],
: {}
};
(function(dm){
for(var i = 0; i < dm.table.rows; i++){
var row = dm.table.rows[i];
for(var j = 0; j < row.cells; i++){
drawCell(i, j);
}
}
//build dm.tree
})(datamodel);
创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,这种机制不会污染全局对象。
2.缓存
设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。
var cache = {},
count = [];
return {
attachSearchBox : function(dsid){
if(dsid in cache){//如果结果在缓存中
return cache[dsid];//直接返回缓存中的对象
}
var fsb = new uikit.webctrl.SearchBox(dsid);//新建
cache[dsid] = fsb;//更新缓存
if(count.length > 100){//保正缓存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox : function(dsid){
if(dsid in cache){
cache[dsid].clearSelection();
}
}
};
})();
CachedSearchBox.attachSearchBox("input1");
3.实现封装
在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问:
var person = function(){
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
print(person.name);//直接访问,结果为undefined
print(person.getName()); // undefined
person.setName("abruzzi"); // default
print(person.getName()); //abruzzi
4.实现面向对象中的对象
闭包的另一个重要用途是实现面向对象中的对象,传统的对象语言都提供类的模板机制,这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的制,但是通过使用闭包,我们可以模拟出这样的机制。
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var john = Person();
print(john.getName()); //default
john.setName("john");
print(john.getName()); //john
var jack = Person();
print(jack.getName()); //default
jack.setName("jack");
print(jack.getName()); //jack
五、内存泄露
1.什么是内存泄露
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
2.哪些会造成内存泄漏
**1.**意外的全局变量
(1)造成原因:
函数中定义变量没有加关键字导致变量成为全局变量,在页面关闭前不会被释放;
this创建的全局变量。
(2)解决方法:
启用严格模式,在文件头部添上"use strict;"。
2.被遗忘的定时器或者回调函数
(1)造成原因:
定时器中有 dom 的引用,即使 dom 删除了,但是定时器还在,所以内存中还是有这个 dom。
(2)解决方法:
-
手动删除定时器和 dom;
-
removeEventListener 移除事件监听。
3.闭包
(1)造成原因:
闭包会使得函数中的变量都被保存在内存中,内存消耗很大。
(2)解决方法:
将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
4.没有清理的DOM元素引用
(1)造成原因:
虽然别的地方删除了,但是对象中还存在对 dom 的引用。
(2)解决方法:
手动删除,elements.btn = null。
六、回收机制
为了找出不再使用的变量,释放其占用的内存,就有了垃圾回收机制:标记清除、引用计数。(引用计数不常用,标记清除用的多些)
1.标记清除
这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
2.引用计数
所谓"引用计数"是指语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
但是引用计数有个最大的问题: 循环引用
function func() {
let obj1 = {};
let obj2 = {};
obj1.a = obj2; // obj1 引用 obj2
obj2.a = obj1; // obj2 引用 obj1
}
当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手动将它们设为空。