理解javascript闭包

https://mp.weixin.qq.com/s/OIgip0WIVn7Lnh3gYISxDQ

https://mp.weixin.qq.com/s/Rf3u_p--3li91U4rKO7iCw

 

javascript的开发中经常会遇到闭包的概念,很多初学者搞不清楚,其实要深入应用还是有点难度的, javascript中很多高级的应用都需要用闭包来实现。

先看两个例子:

例子1   实现一个函数,每次调用后累加变量。

function f(){
    var i=1;
    function m(){
        console.log(i++);
    }
    m();
}

f(); //1
f(); //1
f(); //1

为什么是1呢,我要的是累加啊?你给我这是什么东西,瞬间感觉不开心。

理解上面发生的原因必须要掌握一个概念:那就是javascript函数在被调用后,函数的局部变量就会从内存中释放,只有全局变量会一直保存在内存中,一直到程序执行完毕。

(这时f函数被调用后,存在里面的i就被释放了,m函数就不能解析到值,导致每次解析到i的初始值都是1)

如何正确的输出1,2,3,呢=?

function f(){
    var i=1;
    function m(){
        console.log(i++);
    }
    return m;
}

var c=f();
c(); //1
c(); //2
c(); //3

为什么现在又执行成功了呢?因为函数f实际上是返回函数m,我们把函数f赋值给c,让c再来调用函数m,f函数实际上之被调用了一次,并且把变量存在c中,c()函数指向函数m,多次调用c就达到了依次累加的目的。

当然这样做的目的达到了,却占用了很多系统内存,每次调用c相当于保存了一次全局变量i。如果你的函数够复杂,势必会造成内存的损耗。所以说闭包可以使用,但却不要滥用。

 

例子2  设置一个定时器,每隔2秒打印一个数组中的数字。

var a=[4,6,8,9,12];
for(var i=0;i<a.length;i++){
    setTimeout(function(){
        console.log(a[i]);
    },2000);
}

//undefined
//undefined
//undefined
//undefined
//undefined

[Finished in 2.2s]

执行时间2.2s,并且是一瞬间全部打印出来,从时间上分析,for循环占用了0.2s,定时器占用2s。

这里就要说道同步和异步的区别了,for循环是同步执行,定时器是异步事件,异步事件在同步事件中时,是要先等同步事件执行完毕才能执行异步,for循环执行完毕,只是把变量i赋值成5,定时器执行的时候找不到没有定义的a[5]。

要达到上述需求,我们只需要给异步事件加一个闭包,防止同步事件执行完毕后丢失变量,并且把定时器的时间参数设置成一个变量。

var a=[4,6,8,9,12];
for(var i=0;i<5;i++){
    (function(i){
        setTimeout(function(){
                console.log(a[i]);
            },i*2000);
    })(i);
}

//4
//6
//8
//9
//12

[Finished in 8.2s]

执行时间8.2秒,for循环用时0.2s,定时器用时8s,第一次执行0s(是因为第一次时i的值为0,i*2000当然为0),后面依次相隔2s。

 

闭包的实际解决问题 

闭包其实就是解决外层函数执行完毕后变量丢失的问题,让内部函数能够储存外部函数的变量,不造成外部函数执行后变量丢失。

闭包的正确写法=>

()();

第一个括号填充匿名函数,第二个括号填充需要给匿名函数使用的变量。

 

要理解闭包之前,要首先理解javascript 特殊的变量作用域。

变量的作用域分位两种,全局变量和局部变量。而javascript有别于其他语言的地方在于,函数内部可以读取全局变量。但是在函数外部无法读取函数内部的局部变量。

 

然而有些时候,出于某些原因我们会需要获取函数内部的局部变量,由于前面提到的作用域的限制,外部无法获取内部的作用域,而内部可以获取外部的作用域。因此我们可以通过在函数内部定义一个新的函数的方法来达到目的。

function f1(){
     var hi = "hello, world!";
     function f2(){
          alert(hi); // hello, world!
     }
}

在上面的代码中,函数f2被包括在函数f1内部,这时f1内部所有的局部变量对f2都是可见的。因此,只要把f2作为返回值,就可以在外部获取到f1内部的变量了。

上面代码中的f2函数,就是闭包。

总结:

闭包是能够读取其他函数内部变量的函数。由于在javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

 

作用:

闭包的作用主要体现在两点:

1、读取函数内部的变量;

2、让这些变量的值始终保持在内存中,不会在f1被调用后自动清除。

注意点:

1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗过大,所以不能滥用闭包。否则会造成网页的性能问题,在IE中可能会导致内存泄漏。解决办法是,在退出函数之前,将不使用的局部变量全部删除。

2、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(public method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

 

闭包的优点

  • 避免全局变量被污染
  • 方便调用上下文的局部变量
  • 加强封装性

闭包的缺点

  • 闭包常驻内存,内存消耗很大
  • 可能导致内存泄露

      解决方案:在退出函数之前,将不使用的局部变量全部删除。 

内存泄露及解决方法

闭包创建了循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能造成大量内存得不到回收(内存泄露),因为它们的引用次数永远不可能是 0 。

 比如下面这段代码会导致内存泄露 

window.onload = function(){
    var el = document.getElementById("id");
    el.onclick = function(){
        alert(el.id);
    }
}

当前输出没有问题

解决方法为    

window.onload = function(){
    var el = document.getElementById("id");
    var id = el.id; //解除循环引用
    el.onclick = function(){
        alert(id); 
    }
    el = null; // 将闭包引用的外部函数中活动对象清除
}

内存泄漏及避免策略

//1. 意外的全局变量
//变量bar在foo函数内,但是bar并没有声明.JS就会默认将它变为全局变量,这样在页面关闭之前都不会被释放.
//没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用"严格模式"可以避免.
function doTest2() {
    bar = 2;
    console.log('bar没有被声明!')
}

//2. dom清空时,还存在引用
//很多时候,为了方便存取,经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该DOM节点时,忘记解除对它的引用,则会造成内存泄露.
//与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的
function doTest3() {
    var element = {
        shotCat: document.getElementById('shotCat')
    };
    document.body.removeChild(document.getElementById('shotCat'));// 如果element没有被回收,这里移除了 shotCat 节点也是没用的
}

//3. 定时器中的内存泄漏
//如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 但是 setTimeout ,
//它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.
function fun4() {
    var someResource = getData();
    setInterval(function () {
        var node = document.getElementById('Node');
        if (node) {
            node.innerHTML = JSON.stringify(someResource);
        }
    }, 1000);
}

//4. 不规范地使用闭包
//bar和a形成了相互循环引用.可能有人说bar里不使用console.log(a)不就没有引用了吗就不会造成内存泄露了.
//bar作为一个闭包,即使它内部什么都没有,foo中的所有变量都还是隐式地被 bar所引用。
//即使bar内什么都没有还是造成了循环引用,那真正的解决办法就是,不要将a.fn = bar. function fun5() { var a = {}; function bar() { console.log(a); }; a.fn = bar; return bar; } //避免策略: //减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收(即赋值为null); //程序逻辑,避免“死循环”之类的 ; //避免创建过多的对象 原则:不用了的东西要记得及时归还。 //减少层级过多的引用

 

posted @ 2015-09-18 15:37  南极山  阅读(291)  评论(0)    收藏  举报