美文网首页javascript
闭包(closure)

闭包(closure)

作者: lzyuan | 来源:发表于2017-08-31 10:21 被阅读20次

    ● 闭包基础

    ● 闭包作用

    ● 闭包经典例子

    ● 闭包应用

    ● 闭包缺点

    ● 参考资料

    1、闭包基础

         作用域和作用域链    匿名函数    this的理解与应用 闭包定义


    1.1 作用域和作用域链

    (1)作用域(Scope):作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域(全局变量)和局部作用(局部变量)域两种。

    以下一个例子说明函数的作用域:

    function outFun() {

        var outName = "outName";

        var outNum = "outNum";

        function innerFun() {

            var innerName = "innerName";

            var innerNum = "innerNum";

            alert(outName); // outName

            alert(outNum); // outNum

            alert(innerName); // innerName

            alert(innerNum); // innerNum

        }

        innerFun();

        alert(outName); // outName

        alert(outNum); // outNum

        alert(innerName); // undefined

        alert(innerNum); // undefined

    }
    outFun();

    说明:内部函数(子级函数)innerFun()可以访问外部函数(父级函数)outFun的变量outName、outNum,父级函数不能访问子级函数的变量innerName、innerNum。作用域关乎函数、对象、变量的可访问范围。

    全局作用域(Global Scope)(全局变量):任何地方都能访问的变量或者对象具有全局作用域。例如:

    1)父级函数和父级函数外面定义的变量拥有全局作用域
        var a= 1;
        function f1() {
            var b= 2;
            function f2() {
                var c= 3;
                return a+b+c;
            }
            alert(a); // 1
            alert(b); // 2
            alert(c); // 3
            alert(f2()) ; // 6
        }
        f1();

    说明:父级函数f1可以访问其内部定义的c变量和其外部定义的a、b变量,子级函数f2则可以访问所有变量。其中 a是全局变量,在web页面中全局变量属于 window 对象,全局变量可应用于页面上的所有脚本。所以a变量可以通过window.a获取,f1也可以通过window.f1()获取,一般情况下window对象可以省略不写。

    2)变量声明如果不使用var关键字,那么它就是一个全局变量,即便它在函数内定义
        function f1() {
            var a = "a" ;
            n = " n ";
            function f2() {
                alert(a)
            }
          return f2;
        }
      f1(); // "a"
      alert(n); // "n" 外部可以直接访问

    说明:变量n没有用var关键字定义,那么它就是个全局变量,在函数外可以引用。

    3)所有window对象的属性拥有全局作用域
    一般情况下,全局变量属于 window 对象,全局变量可应用于页面上的所有脚本。window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等。

    局部作用域(Local Scope)(局部变量):在固定的代码片段内访问,一般在函数内部,也称为函数作用域。

    例如,上面的第一个例子中,在函数f1内部定义的变量b,只能在函数内部使用,在函数外部或脚本代码是不可用的。

    全局变量和局部变量即便名称相同,它们也是两个不同的变量。修改其中一个,不会影响另一个的值。

    变量生命周期:全局变量的作用域是全局性的,即在整个JavaScript程序中,全局变量处处都在,不会自动在内存中清除。而函数内部生命的局部变量的作用域是局部性的,只有在函数内部起作用,当函数运行过程中对局部变量引用结束之后,局部变量就会在内存中清掉。

    (2)作用域链(Scope Chain):创建函数的同时,它的作用域也被创建了。作用域中包含可访问的数据对象的集合,称为作用域链,它决定了哪些数据可以被函数访问。

    function Add(num1, num2) {

         var sum = num1 + num2;

         return sum;

    }

    在函数Add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量。函数Add的作用域在运行时用到:

    var total = Add(5, 7);

    部分全局对象

    执行此函数时会创建一个称为“运行期上下文( execution context )”的内部对象,它定义了函数执行时的环境,并被初始化为当前运行函数的[[Scope]]所包含的对象。

    根据局部变量、命名参数、参数集合以及this等在函数中的出现顺序,它们被复制到“运行期上下文”的作用域链中,它们共同组成了一个新的对象,叫“活动对象( activation object )”:

    活动对象与部分全局对象

    在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,即从活动对象开始搜索,查找同名的标识符,若找到就使用这个标识符对应的变量,若没找到继续搜索作用域链中的下一个对象。如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。

    (3)作用域链和代码优化

    从作用域链的结构可以看出,在运行期上下文的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于运行期上下文作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。

    如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用, 例如:

    function changeColor() {

        document.getElementById("button").onclick = function () {

            document.getElementById("button").style.backgroundColor = "red";

        }

      }

    两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。

    function changeColor() {

        var doc = document;

        doc.getElementById("button").onclick = function () {

            doc.getElementById("button").style.backgroundColor = "red";

        }

    }

    这段代码比较简单,重写后不会显示出巨大的性能提升,但如果程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善.

    1.2 匿名函数

    (1)匿名函数:没有给函数命名的函数。

    (2)函数的定义有三种:

    1)最常见的一种

    function f1(x){

        return x;

    }

    2)使用了Function构造函数,把参数列表和函数体都作为字符串。不建议使用:

    var f1 = new Function() { 'x', 'return x;' } ;

    3 ) 通过匿名函数赋值,不推荐通过匿名函数赋值

    var f1 = function(x) { return x; }

    (3)匿名函数的创建有两重方法:

    1)没有函数名:

      function(){};

    2)通过两个括号实现:

    ( function (x, y) {

        return x+y;

    })(2, 3);

    其中,第一个括号是对匿名函数的定义;第二个括号是对匿名函数的调用。

    (4)匿名函数的作用

    1)创建闭包,构建命名空间,以减少全局变量的使用

    var allObj =  { };

    (function(){

        var addEvent =  function() {

            function removeEvent() {

                allObj.addEvent = addEvent ;

                allObj.removeEvent= removeEvent ;

            }

        }

    })( );

    在这段代码中, 匿名函数中的函数对象addEvent和匿名函数对象removeEvent都是局部变量,但我们可以通过全局变量allObj使用它,这就大大减少了全局变量的使用,增强了网页的安全性。

    2)通过两个括号的匿名函数赋值,比较实用

    var addFun = (function(x, y){

        return x+y;

    })(2, 3);

    创建了一个addFun函数,并通过匿名函数将其初始化为5。

    3)递增效果,闭包能使函数的内部变量保存在内存中。

    var outer = null ;

    (function(){

        var a = 1;

        function inner() {

              a +=1;

              alert( a );

        }

        outer = inner;

    });

    outer(); //2
    outer(); //3
    outer(); //4

    我们要想使用此段代码:oEvent.addEvent(document.getElementById('box') , 'click' , function(){});

    1.3 this理解与应用


    在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。以下列出this出现的集中场景:

    (1)有对象就指向调用对象

    var object = {

        value: 123,

        getValue: function(){

            return this.value; // this指向object

       }

    }

    object.getValue(); // 调用getValue的对象为object.

    (2)没有调用对象就指向全局对象

    var myObj = {

        value: 123,

        getValue: function() {

              var foo = function(){

                        console.log( this.value ); // undefined 指向window

               }

              foo(); //没有调用对象

              return this.value; // this指向object

         }

    }

    console.log(myObj.getValue()); //123 调用getValue的对象为object.

    (3)用new构造就指向新对象

    js 中,我们通过 new 关键词来调用构造函数,此时 this 会绑定在该新对象上。

    var SomeClass =function() {

        this.value = 100;

    }

    var myCreate =new SomeClass();

    console.log(myCreate.value); // 输出100

    (4) 通过 apply 或 call 或 bind 来改变 this 的所指

    // apply 和 call 调用以及 bind 绑定: 指向绑定的对象

    // apply 方法接受两个参数:第一个是函数运行的作用域, 另外一个是一个参数数组(arguments)。

    // call 方法第一个参数的意义与 apply() 方法相同, 只是其他的参数需要一个个列举出来。

    // 简单来说, call 的方式更接近我们平时调用函数, 而 apply 需要我们传递 Array 形式的数组给它。 它们是可以互相转换的。

    var myObject = { value: 100 };

    var foo =function() {

        console.log(this);

    };

    foo();// 全局变量 this指向window

    foo.apply(myObject);// { value: 100 }

    foo.call(myObject);// { value: 100 }

    var newFoo = foo.bind(myObject);

    newFoo();// { value: 100 }

    1.4 闭包(closure)定义


    (1)闭包:简而言之,闭包就是函数中的函数。

    function f1(){

        var a = 666;

        function f2() {

            alert(a);

        }

    }

    (2)闭包特点:

    1)子级f2()可以向上访问父级f1()的所有变量,而父级f1()不能向下访问子级f2()的变量,即外部不能访问内部变量,内部可以访问外部变量,这就是链式作用域。

    2)子级函数可以引用父级函数的定义的变量,但该变量是最终的结果。

    <ul>

        <li></li>

        <li></li>

        <li></li>

        <li></li>

    </ul>


    var lists = document.getElementsByTagName('li');

        for(var i = 0 , len = lists.length ; i < len ; i++){

            lists[ i ].onmouseover = function(){

                alert(i);

            };

        }


    说明:当鼠标移过每一个非自执行的匿名函数function(){ alert(i); })时,会首先在内部查找是否定义了i,结果是没有定义;因此它会进一步向上查找,查找结果是已经定义了,并且i的值是4(父级循环结束后的i值,因为匿名函数没有自执行);所以,最终每次弹出的都是4。

    (3)匿名函数本身是个闭包,可以在函数外部对函数内部的变量进行操作。

    2、闭包作用


    (1)通过闭包,可以读取函数内部的变量,并且闭包能让函数的内部变量(局部变量)始终保存在内存中,即使是在父级函数关闭(运行结束)的情况下。

    function f1(){

        var n=666;

        add = function( ) { n+=1 } ; //全局变量

        function f2( ) {

             alert (n);

        }

        return f2;

    }

    var result = f1( );

    result( ); // 666 读取函数内部变量

    add(); // 读取函数内部变量

    result(); // 667 函数的内部变量始终保存在内存中

    上面函数中,result实际上就是闭包f2函数,它一共运行了两次,第一次是666,第二次是667。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

    3、闭包经典例子


    例1:

    var name = "The Window";

        var object = {

            name : "My Object",

            getNameFunc : function(){

                   return funtion(){

                           return this.name; // this指向window

                  }

          }

    alert(object.getNameFunc()()); // The Window

    例2:

    var name = "The Window";

    var object = {

        name : "My Object",

        getNameFunc : function(){

            var that = this; // this指向object

            return function(){

                return that.name; // that指向object

            };

        }

    };

    alert(object.getNameFunc()()); //My Object

    4、闭包应用


    4.1、循环与闭包

    var k = document.getElementsByTagName("li");

    for(var i = 0; i < k.length; i ++) {

        (function(i){

            k[i].onclick = function() {

                alert(i);

            }

        })(i)

    }

    5、闭包缺点


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

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

    参考资料


    JavaScript中的匿名函数及函数的闭包  http://www.cnblogs.com/rainman/archive/2009/05/04/1448899.html#m0 

    JavaScript开发进阶:理解JavaScript作用域和作用域链  http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html 

    javascript中局部变量和全局变量的区别详解 http://www.jb51.net/article/61442.htm

    hJavaScript中作用域、闭包与this指针  http://blog.csdn.net/junbo_song/article/details/52261247 

    hJavaScript闭包详情  http://www.cnblogs.com/gbin1/p/4092427.html 

    理解Javascript的闭包 http://coolshell.cn/articles/6731.html 

    学习javascript的闭包 http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html 

    JavaScript闭包 http://www.runoob.com/js/js-function-closures.html

    JavaScript内存泄露  http://www.cnblogs.com/rainman/archive/2009/03/07/1405624.html

    在函数执行时,this 总是指向调用该函数的对象。要判断 this 的指向,其实就是判断 this 所在的函数属于谁。

    相关文章

      网友评论

        本文标题:闭包(closure)

        本文链接:https://www.haomeiwen.com/subject/zyeputtx.html