JavaScript闭包

闭包这个东西确实好用,理解他对学习JavaScript确实很有帮助。

闭包的内部细节,依赖于函数被调用过程所发生的一系列事件为基础,所以有必要先弄清楚以下几个概念:1. 执行环境和活动对象、2. 作用域链。

在javascript中,执行环境可以抽象的理解为一个object,它由以下几个属性构成:

executionContext:{
variable object:vars,functions,arguments,
scope chain: variable object + all parents scopes
thisValue: context object
}

作用域链,它在解释器进入到一个执行环境时初始化完成并将其分配给当前执行环境。每个执行环境的作用域链由当前环境的变量对象及父级环境的作用域链构成。

详情可查看我的博客

什么是闭包?

“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。通俗一点的讲就是闭包是指有权访问另一个函数作用域变量的函数,创建闭包的通常方式,是在一个函数内部创建另一个函数。

理解JavaScript的作用域机制对理解闭包有很大的帮助。

上文我们提到了,由于作用域链的结构,外围函数是无法访问内部变量的,为了能够访问内部变量,我们就可以使用闭包,闭包的本质还是函数

一个简单的例子:

        function A(){            
            var x = 25;            
            return function(){                
                console.log(x);
            }
        }        
        var n = A();
        n();//25

闭包实现原理

当某个函数调用时会创建一个执行环境以及作用域链,然后根据arguments和其它命名参数初始化形成活动对象。 在外部函数调用结束后,其执行环境与作用域链被销毁,但是其活动对象保存在了闭包之中,最后在闭包函数调用结束后才销毁

普通函数:在outer()执行完,局部变量local被销毁,内存仅仅保存全局作用域。

function outer() {

    var localVal = 30;

    return localVal;

}

outer(); // 30

闭包:在outer()执行后,func()仍然可以访问outer()内部的localVal,因为func()将outer()的内的活动对象(localVal)添加到了func()的作用域链。在outer()执行后,由于localVal被func()的作用域链所引用,所以localVal不会被销毁,而是存在内存中,直到func()被销毁,才会随之销毁。

function outer() {

    var localVal = 30;

    return function() {

        return localVal;

    }

}

var func = outer();

func(); // 30

闭包的实现

在函数中定义函数,并且内部函数引用了外部函数的变量,最后内部函数被返回

闭包的作用

我们的函数可产生类似于块级作用域的东西,内部的变量外部不可访问,但是我们需要提供访问的接口,这个接口的实现便依赖于我们的闭包

闭包的问题

闭包的使用上需要注意,因为他会增大内存的负担,对性能有一点影响,另外闭包有可能会有一些容易出错的场景。

再看一道题目:

var str1 = str2 = "web";

(function () {

var str1 = str2 = "前端";

})();

console.log(str2);

当然这里输出的是结果是: 前端

如果现在将console.log(str2)变成consloe.log(str1),那么输出的结果就会变成 web

解答

其实这题并不是很难,第一句 var str1 = str2 = "web"; 其实是定义了两个全局的变量,在利用function的闭包内用 var 重新定义了 str1 而没有重新定义 str2

我们知道在默认情况下 如果不用 var 定义的变量都会变成全局变量,所以此时在function闭包内的str2就是引用了全局变量,所以赋值操作当然也就能赋予全局变量 str2 所以输出 str2 结果是 "前端"

而 str1 用了 var 定义,就是在function闭包内的变量,闭包外自然不可以改变,所以输出的结果是 "web"

脑洞大开

其实每一次看到这种形式的代码

(function(){

})()

都觉得非常的新鲜,觉得这里有很多东西可以专研,所以在这里也总结一下这种形式的闭包。

解释前先看看

首先这种形式的闭包是人为的加上去,并不是说可以有什么神奇的 duangduang 的特效,而是可以避免很多本来是局部变量可以搞定的比较 low 的变量去污染全局的变量

其次在js中,是没有块作用域 这种说法

首先我们回到C++,如果有一段代码是这样

    int number1=10;

    if(true){

      int number1 = 5;

    }

    cout<<number1;

这里的结果还是 10

而在js代码之中

    var str1= "web";

    if(true){

        var str1="前端";

    };

    console.log(str1);

这里的结果就是 前端

因为 if{} 没有块作用域,所以内部的str1直接就重定义了外部全局的str1,所以输出的结果就只是"前端"了,而且这也污染了全局变量(那个等于"web"的str1已经不见了踪影)

而大 js 只有 函数作用域

所以我们要利用函数闭包

利用函数闭包能有效的封装局部的变量,而不污染全局作用域

    str1 = "web";

    (function () {

         var str1 = "前端";

         //str1剩下的功能

    )();

    console.log(str1);

此时输出的还是 web

所以我们利用了一个匿名函数 function(){} 并且让他自己调用自己执行函数内部的操作 并且 str1 也没有污染到外部的全局作用域

接下来用一些题来加深理解:

  例1:判断作用域指向的变量对象是否相同

        function A(){            
            var x = 1;            
            return function(){
                x++;                
                console.log(x);
            }
        }        
        var m1 = A();//第一次执行A函数
        m1();//2
        m1();//3
        var m2 = A();//第二次执行A函数
        m2();//2
        m1();//4

  每次执行 A 函数时,都会生成一个A的活动变量和执行环境,执行完毕以后,A 的执行环境销毁,但是活动对象由于被闭包函数引用,所以仍然保留,所以,最终剩下两个A的变量对象,因此 m1 和 m2 在操作x时,指向的是不同的数据,

  注意:执行环境和变量对象在运行函数时生成;执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)

  例2:

        function A(){            
            var x = 1;            
            var m=[];
            m[0] = function(){
                x++;                
                console.log(x);
            };
            m[1] = function(){
                x++;                
                console.log(x);
            }             
                return m;
        }        
       var m = A();//第一次运行A,而且只运行这一次
        m[0]();//2
        m[1]();//3
        m[0]();//4
        m[1]();//5

  这个例子和刚刚十分类似,不同的是,在A内部就先定义了两个函数,可以看出 ,最后的结果与上面的例子有些不同:变量x仍然能保持递增,但是m[0]和m1定义的函数,对于x的改变不再是相互独立的,其实大家估计猜到了,这里的m[0]和m1的作用域指向的A的变量对象,其实是同一个,为什么呢?很简单,看看刚刚这段代码,其实是只调用了一次A函数,再看上文那句话:执行环境和变量对象在运行函数时生成

  既然A只执行一次,那么A的活动变量当然也就生成了一个,所以这里m[0]和m1的作用域指向同一个A的变量对象

  例3:判断变量对象中变量的值

         function A(){            
             var funs=[];            
             for(var i=0;i<10;i++){
               funs[i]=function(){                   
                 return i;
               }
            }            
            return funs; 
        }        
        var funs = A();//定义funs[0]-funs[9],10个函数
        console.log(funs[0]());//10
        console.log(funs[1]());//10
        console.log(funs[6]());//10

  这个例子其实算是一个经典案例,在很多地方都有提到,执行完毕后 funs 数组中,funs[0]-funs[9]存的其实都是一样的,都是一个返回i值的函数,这个例子容易错误的地方其实在于,弄错了产生执行环境的时机,还是看这句话:执行环境和变量对象在运行函数时生成

  所以,当执行var funs = A();时,只是定义函数,而没有执行,真正产生环境变量的时间是在console.log(funs[0]());这三句的时候,此时A的变量对象中i值是什么呢?很简单,看它return的时候,i的值,显然,i的值是10,所以,最后三句输出的都是10

  如果想让 fun[i] 能够返回 i:

         function A(){            
             var funs=[];            
             for(var i=0;i<10;i++){
               funs[i] = function anonymous1(num){                            
                            return function anonymous2(){                        
                         return num;
                    }
                }(i);
            }            
             return funs; 
        }        
        var funs = A();//定义funs[0]-funs[9],10个函数
        console.log(funs[0]());//0
        console.log(funs[1]());//1
        console.log(funs[6]());//6 

  是不是一看头就大了?没关系,接下来我们慢慢分析,当然,上述代码中 anonymous1 和 anonymous2 两个名字是我自己添加上的,为了后面能够更好的说明。

  首先,先来看看 function anonymous1(num){}(i),这是一个立即执行函数,效果和名字一样,定义完之后马上运行结果,那这里运行的结果是什么呢?就是把i的值立即传递给 num 这个局部变量,然后再返回 anonymous2,请注意这个立即执行函数被执行的次数,10次,[即使创建一个函数让它执行的时候创建自己的活动对象]再来看看这句话:执行环境和变量对象在运行函数时生成。

  这里面生成了几个 anonymous1 的活动变量?当然也是10个。

  那每个 anonymous1 活动变量中存贮的 num 值是多少?看anonymous函数return的时候可以知道,存贮的num值就是每次传入的i值,也就是0-9。

  好了,那现在很明了了,这样的写法其实相当于,把每次的 i 值都保存在一个 anonymous1 活动变量钟,给最内层的 anonymous2 函数使用

四个例子理解闭包

/**
* 闭包原理
* @date   2017-04-10 14:04:17
* @version 1
*/
//理解作用域、作用域链
//内部作用域可以通过作用域链引用外部作用域的变量
// function(){}() functionName() 立即执行函数

//例一
function exp1() {
    var a = 1;
    var b = function() {
        //console.log(0); //exp1函数被调用的时候,0不会输出;b函数被调用时,0才输出。
        console.log(a); 
    }; //b是函数,被调用的时候输出才执行,通过作用域链找到a值。
    a = a + 3;
    return b;
}
var res1 = exp1(); //[function]
res1(); //调用函数执行输出,a值已经被修改为4
console.log('------------------------');
//本质:exp1函数已执行完, b函数才被调用 。

//例二
function exp2() {
    var a = 1;
    var b = function() {
        console.log(a); 
    }(); //b是数值,匿名函数立即执行输出a值为1
    a = a + 3;
    return b;
}
var res2 = exp2(); //1
console.log('------------------------');

//例三
function exp3() {
    var a = 1;
    var b = function() {
        return function(){
            console.log(a); 
        };
    }(); //再加一层闭包保持b是函数。外层匿名函数立即执行返回函数,b是函数,被调用的时候输出才执行,通过作用域链找到a值。
    a = a + 3;
    return b;
}
var res3 = exp3(); //[function]
res3();//调用函数执行输出,a值已经被修改为4
console.log('------------------------');

//例四
function exp4() {
    var a = 1;
    var b = function(num) {//参数num被赋值为1
        return function(){
            console.log(num);
        };
    }(a);//再加一层闭包保持b是函数,外层匿名函数传参立即执行。立即执行外层匿名函数返回函数,b是函数,被调用的时候输出才执行,通过作用域链找到num值。
    a = a + 3;
    return b;
}
var res4 = exp4(); //[function]
res4();//调用函数执行输出,num值为1
console.log('------------------------');

//拓展
function exp5() {
    var b = [];
    for (var i = 0; i < 4; i++) {
        b[i] = function() {
            console.log(i);
        };
    }
    return b;
}
var res5 = exp5(); //[[function],[function],[function],[function]]
res5[1](); //4
console.log('------------------------');

function exp6() {
    var b = [];
    for (var i = 0; i < 4; i++) {
        b[i] = function(num) {
            return function(){
                console.log(num);
            };
        }(i);
    }
    return b;
}
var res6 = exp6(); //[[function],[function],[function],[function]]
res6[1](); //1

参考:

征服 JavaScript 面试:什么是闭包?

https://segmentfault.com/a/1190000005013790

https://segmentfault.com/a/1190000007077713

《JavaScript高级程序设计》

-20170425补充

js闭包其实不难,你需要的只是了解何时使用它