JS 闭包
JS编程的时候你一定遇到过这个问题:局部变量实现累加,看下面例子:
function aotuadd(){
var a=1;
a++;
console.log(a);
}
aotuadd();//2
autuadd();//2
上面的代码无法实现累加,这时可能有的人就会选择把a放在全局作用域中,能实现累加功能,但是会使全局变量增多,这是我们不想看到的。
其实之所以把a放在全局作用域中,是因为autoadd函数的作用域被全局作用域包裹,所以我们可以在全局作用域中取值;
那么我们是不是可以给autoadd外层再包裹一个作用域(假设是wapper),然后将这个a放在wapper作用域中,问题不就解决了嘛。
我们既能访问wrapper中的a,又不必增加全局变量。因为js中只有函数能够产生作用域,所以其实就是再aotoadd外包裹一个wrapper函数,试着写一下:
function wrapper(){
var a=1;
function autoadd(){
a++;
console.log(a);
}
}
wrapper();
写到这里发现,我们无法访问autoadd,怎么解决:
function wrapper(){
var a=1;
function autoadd(){
a++;
console.log(a);
}
window.bar=autoadd;
}
wrapper()
bar();//2
bar();//3
上面这种方法是能够解决无法调用的问题的,但是这回到了我们最开始遇到的问题,增加了全局变量/函数,这是我们不想看到的;另一种解决方法:
function wrapper(){
var a=1;
function autoadd(){//必要条件
a++;//必要条件
console.log(a);
}
return autoadd;
}
var x=wrapper()//返回一个函数,巧合的是,返回的这个函数体中,还有一个变量a要引用wrapper作用域下的a,所以这个a不能销毁,wrapper()上下文环境不被销毁,依然存在于执行上下文栈中;
x();
x();
通过返回函数的方法进行调用,上面的这种写法就是我们最常见的闭包的写法,也就是说,闭包的产生,其实并不是一定依赖于“返回函数”这个条件,只不过不通过这种方法调用有违初衷;
看懂了上面这个例子,闭包的概念也呼之欲出:闭包是指有权访问另一个函数作用域中的变量的函数,即当前作用域总能访问外部作用域中的变量。
你可能经常看到这句话:“创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量”。其实我觉得恰恰就是这句话,导致很多人无法理解闭包,换成下面这种说法更好理解:创建闭包的最常见的方式是在一个函数外部包裹另一个函数,通过在另一个函数内部定义变量的方式,使我们想要的变量驻留在外层函数中,减少全局变量。
闭包的两个必要条件:函数外层有函数/ 内层函数要使用外层函数中的变量
我们总结一下,上面var x=wrapper()和function warpper()可以利用立即执行函数合写,进一步减少全局变量:
var x=(function(){
var a=1;
return function(){
a++;
console.log(a);
}
})();
x();//2
x();//3
x=null;//解除引用,等待垃圾回收
或者:
function Myobj(){
var age=1;;
this.autoadd=function(){
age++;
console.log(age);
//return age;
}
}
var obj=new Myobj();
obj.autoadd();
obj.autoadd();
另一个常见问题:
for循环给网页中一连串元素绑定,例如onclick事件:
var fn = function() {
var divs = document.querySelectorAll('div');
for (var i = 0; i < 3; i++) {
divs[i].onclick = function() {
alert(i);
};
}
};
fn();
点击每个div都会弹出3。这是为什么呢?
我们先来分析一下原因:onclick事件是一个异步回调函数的指针,并不会立即执行,上面的函数表达式,并不会进行变量赋值。只有在调用一个函数时,一个新的执行上下文才会被创建出来。那么我们是不是可以通过调用函数的方法,来创建多个新的执行上下文环境,创建新的作用域,这样不同的调用就可以有不同的参数。那么解决思路也是外层包裹function的方法,即利用闭包:
var fn = function() {
var divs = document.querySelectorAll('div');
for (var i = 0; i < 3; i++) {
divs[i].onclick = (function(a) {//立即执行函数,创建新的执行上下文
alert(a);
})(i);
}
};
fn();
或者:
var fn = function() {
var divs = document.querySelectorAll('div');
for (var i = 0; i < 3; i++) {
(function(i){//立即执行函数,创建新的执行上下文,将i驻留在内存中
divs[i].onclick = function() {
alert(i);
})(i);
}
};
fn();
另一种解决方法:(利用事件代理)
var ul=document.querySelector('ul');
var lis=ul.querySelectorAll('ul li');
ul.addEventListener('click', function (e) {
var target= e.target;
if(target.nodeName.toUpperCase()==='LI'){
alert([].indexOf.call(lis,target));
}
},false)
理解闭包的关键就是下面这句:
闭包:当一个函数在定义它的作用域以外的地方被调用时,它访问的依然是定义它时的作用域。这种现象称之为闭包。
JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。——《JavaScript语言精粹》
闭包的优点:
1)使变量驻留在内存中(多了变缺点);
2)避免全局变量污染;
3)私有化变量;
闭包的缺点:
1)因为闭包会携带包含它的函数的作用域,所以比其他函数占用更多内存;
2)使用不当会造成内存泄漏;
闭包应用场景(来自《javascript高级程序设计》)
1.使用闭包可以在JS中模拟块级作用域(ECMAScript6标准之前的JavaScript本身没有块级作用域的概念);
function outputNumbers(count){
(function(){
for(var i = 0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误!
}
2.闭包可以用于在对象中创建私有变量;
// 1.2.闭包可以用于在对象中创建私有变量
function MyObject(){
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
// 特权方法,调用私有方法、函数
this.publicMethod = function(){
privateVariable++;
return privateFunction();
}
}
闭包的运用
1.匿名自执行函数
我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:
//将全部li字体变为红色
(function(){
var els = document.getElementsByTagName('li');
for(var i = 0,lng = els.length;i < lng;i++){
els[i].style.color = 'red';
}
})();
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,
因此els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。
2. 实现封装/模块化代码
var person= function(){
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
}();
console.log(person.name);//直接访问,结果为undefined
console.log(person.getName()); //default
person.setName("jozo");
console.log(person.getName()); //jozo
3. 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,
我们可以模拟出这样的机制。还是以上边的例子来讲:
function Person(){
var name = "default";
return {
getName : function(){
return name;
},
setName : function(newName){
name = newName;
}
}
};
var person1= Person();
print(person1.getName());
john.setName("person1");
print(person1.getName()); // person1
var person2= Person();
print(person2.getName());
jack.setName("erson2");
print(erson2.getName()); //person2
Person的两个实例person1 和 person2 互不干扰!因为这两个实例对name这个成员的访问是独立的 。
初学js很多理解不到位的地方,望批评指正!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。