2

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很多理解不到位的地方,望批评指正!


不能写bug啊
142 声望11 粉丝

下一篇 »
JS观察者模式