Javascript 閉包

如果要了解閉包,我們需要先了解閉包的由來,閉包的產(chǎn)生,源于JS的詞法作用域

詞法作用域

作用域是指一個(gè) 變量能夠訪問到的區(qū)域 例如我們?cè)O(shè)置一個(gè)var n=0;實(shí)際分兩步,聲明和賦值,var n;n=1;在js中只有函數(shù)能夠限定作用域,除此之外聲明的都是全局作用域,在ES6的新標(biāo)準(zhǔn)中,采用了let和const來限定局部變量,注意局部變量的優(yōu)先級(jí)是高于全局變量的

執(zhí)行環(huán)境是JavaScript中最為重要的一個(gè)概念,在js代碼開始執(zhí)行的時(shí)候,會(huì)創(chuàng)建一個(gè)匿名函數(shù)的執(zhí)行環(huán)境,執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其它數(shù)據(jù),每個(gè)執(zhí)行環(huán)境都有一個(gè)與之相關(guān)的變量對(duì)象,環(huán)境中定義的所有變量和函數(shù)都保存在這個(gè)對(duì)象中,雖然我們編寫的代碼無法訪問這個(gè)對(duì)象,但解析器咋及處理數(shù)據(jù)的時(shí)候會(huì)在后臺(tái)使用它

變量的作用域是在定義的時(shí)候決定的而不是在執(zhí)行的時(shí)候決定的,變量只能在本層作用域和上層作用域生效,不能在下層作用域生效

舉一個(gè)栗子來加深理解

    var num;
    function f1(){
        num=3;
    }
    f1();
    console.log(num);//3

在這個(gè)栗子中,num的值在全局最開始設(shè)置的是undefined,但是在函數(shù)f1中改變了該值,這個(gè)值變?yōu)?,在函數(shù)f1執(zhí)行了一次之后我們?cè)俅蛴um的值,得到的是num=3,通過解讀JavaScript是詞法作用域我們可以知道,num這個(gè)變量的作用域是在它定義的時(shí)候決定了它是一個(gè)全局變量,所以即使在函數(shù)中執(zhí)行num的操作,我們?cè)诤瘮?shù)外部依然可以獲取到這個(gè)值

注意一個(gè)細(xì)節(jié),獲取全局變量比獲取局變量要耗費(fèi)性能,舉個(gè)例子:

    function (obj){
        var i=0,
            l=obj.length;
        for(;i<l;i++){}
    }

在這段代碼里,我們獲取設(shè)置局部變量obj.length用來進(jìn)行循環(huán)條件判斷,使用i<l是一個(gè)比i<obj.length更佳的選擇,因?yàn)槿绻鹢bj包含大量的代碼,我們?cè)谘h(huán)時(shí)每一次都要重新獲取obj的值,而設(shè)置l則可以降低這部分的性能損耗

閉包

在JS中只有function能形成塊級(jí)作用域,在函數(shù)內(nèi)部的變量外部是無法獲取的,而在函數(shù)中嵌套函數(shù),被嵌套的函數(shù)則可以拿到外層函數(shù)的變量的值,所以有的時(shí)候我們需要拿到一個(gè)函數(shù)的內(nèi)部數(shù)據(jù)時(shí),可以采用如下的方法:

    function A(){
        var name="tom"

        function B(){

            return name
        }

        return B();

    }

    console.log(A());//tom

以上的代碼就是我們常說的閉包,官方的閉包的解釋十分的概念化,我對(duì)閉包的理解就是:

  1. 初步的理解:能夠獲取其他函數(shù)的內(nèi)部數(shù)據(jù)的函數(shù)
  2. 深入的理解:函數(shù)記住并訪問其所在的詞法作用域,叫作閉包現(xiàn)象,而此時(shí)函數(shù)對(duì)作用域的引用叫作閉包(閉包就是引用,維基上有一段對(duì)閉包的引用解釋的我覺的是比較好:引用了自由變量的函數(shù),自由變量和函數(shù)將一同存在),如果想要理解這一塊,我們需要對(duì)JavaScript中的垃圾回收機(jī)制JavaScript的存儲(chǔ)機(jī)制做一些了解
JavaScript的垃圾回收機(jī)制

垃圾回收的主要工作是跟蹤內(nèi)存的分配和使用,在內(nèi)存不再工作時(shí),將其進(jìn)行釋放

在垃圾回收機(jī)制中,涉及到的算法比較多,所以也不打算深入,只做簡(jiǎn)單的了解即可,我們只需要知道:

在js中,在創(chuàng)建變量或函數(shù)時(shí),會(huì)開辟空間,有一個(gè)屬性來標(biāo)注它們是否被其它變量,被引用則計(jì)數(shù)器++,

  1. 如果函數(shù)被調(diào)用過了,并且以后不會(huì)再用到,此時(shí)判斷計(jì)數(shù)器為0,那么垃圾回收機(jī)制就會(huì)將其作用域進(jìn)行銷毀
  2. 全局變量是不會(huì)被銷毀的
JavaScript的存儲(chǔ)機(jī)制

我們都知道,在JS的存儲(chǔ)中是分為堆存儲(chǔ)和棧存儲(chǔ)的,堆存儲(chǔ)是用來存儲(chǔ)對(duì)象,也就是引用數(shù)據(jù)類型,而棧存儲(chǔ)是用來存儲(chǔ)簡(jiǎn)單數(shù)據(jù)類型,在這里我們需要了解的是:

  1. 對(duì)象的引用是通過地址來傳遞的,一個(gè)對(duì)象應(yīng)用另一個(gè)對(duì)象后只是它的地址指向發(fā)生了改變,而它原來的值是依然存在的
  2. 簡(jiǎn)單數(shù)據(jù)類型的引用是直接在棧中把原先數(shù)據(jù)進(jìn)行替換的

了解了以上兩個(gè)概念我們可以再回頭看一下關(guān)于深入的理解閉包,如果一個(gè)函數(shù)被引用,那么它的作用域就不會(huì)被垃圾回收機(jī)制進(jìn)行銷毀,這就可以理解為記住了這個(gè)作用域,如果對(duì)其內(nèi)部的變量進(jìn)行訪問,那么稱為訪問

<script type="text/javascript" language="javascript">
    function a(){
        var i=0;
        return function b() {
            alert(++i);//記住并訪問
        }
    }
    c=a();//引用
    c();
</script>

由于比包內(nèi)的函數(shù)對(duì)外部函數(shù)的變量進(jìn)行了引用,在垃圾回收機(jī)制看來,引用計(jì)數(shù)不為0,所以在函數(shù)的作用域被銷毀后,閉包內(nèi)的變量會(huì)一直存在,這也是閉包的缺點(diǎn),大量的閉包引用如果在引用后沒有賦值為null會(huì)占用大量的內(nèi)存空間,在IE低版本中會(huì)導(dǎo)致內(nèi)存泄漏

閉包還有一個(gè)特性是閉包只能氣的包含函數(shù)中任何變量的最后一個(gè)值,例如一些常見的面試題中經(jīng)常出現(xiàn)這個(gè)問題

    function person(){

        var arr=[];

        for(var i=0;i<10;i++){
           arr[i]=function (){
                return i;
            };
        };
        return arr;
    }
    
    console.log(person()[1]());//10

在上面的代碼中arr的每個(gè)位置存儲(chǔ)的都是10,如果我們想要使每個(gè)返回不同的數(shù)字那么我們需要對(duì)代碼進(jìn)行改進(jìn)

    function person(){

        var arr=[];

        for(var i=0;i<10;i++){
           arr[i]=function (num){
                return function(){//再次添加一個(gè)閉包,在這個(gè)閉包內(nèi)將每次i的值進(jìn)行存儲(chǔ)
                    return num;
                };
            }(i)
        };
        return arr;
    }
    console.log(person()[1]());//10

閉包的用途

  1. 獲取其它函數(shù)內(nèi)部的變量的值

  2. 緩存變量的值

        function A(){
            var i=1;
            return function(){
                i++;
                console.log(i);
            }
        }
        var a=A();
        a();
        a();
        a();
      //這里我們要探討一個(gè)問題,閉包緩存數(shù)據(jù)的形式是什么,之前一直對(duì)閉包的概念有誤解,一直以為A事必報(bào),但是實(shí)際上
      // return function(){
      //          i++;
      //          console.log(i);
      //     }
      //這部分才是真正的閉包,所以能夠進(jìn)行緩存的是這一部分,這也就解決了之前的一個(gè)疑惑,為什么需要寫
      // var a=A();這一步再調(diào)用a();才能看到閉包緩存的數(shù)據(jù)
      //我們可以根據(jù)這個(gè)特性座椅計(jì)時(shí)器,來記錄一個(gè)構(gòu)造函數(shù)創(chuàng)建了多少個(gè)對(duì)象
       var C=function(){
    
            var i=0;
    
            return function(){
    
                return ++i
            }
        }();//讓函數(shù)自調(diào)用自身
    
        function fn(name){
    
            if(this==window){//判斷當(dāng)前調(diào)用的是不是window對(duì)象
                return 
            }
    
            this.name=name;
    
            fn.cn=C();//注意,在這里實(shí)際上已經(jīng)相當(dāng)于直接使用++i為fn.cn賦值了
        }
    
        var a=new fn("tom");
        var b=new fn("son");
        var c=new fn("xm");
    
        console.log(fn.cn)//3
    
  3. 設(shè)置變量的可讀可寫(封裝)

        function A(_name){
            var name=_name;
    
            return {
                getName:function(){//可讀
                    return name;
                },
                setName:function(newName){//可寫
                    name=newName; 
                }   
    
            }
        }
    
        var a=new A("tom");
        console.log(a.getName())//tom
        a.setName("xm");
        console.log(a.getName())//xm
      
      //還有一個(gè)寫法
      
        function A(_name,_age){
            var name=_name,
                age=_age;
    
            return {
                name:function(value){
                    if(value===undefined) return name;
                    else name=value;
                },
                age:function(value){
                    if(value===undefined) return age;
                    else age=value;
                }
            } 
        }
        
        var a=new A("tom",18);
        console.log(a.name());//tom 
        a.name("xm");
        console.log(a.name());//xm
    
  4. 沙箱模式

    (function(){})();//防止代碼污染
    //這里我們需要提到一個(gè)概念,也就是JS中的依賴注入,和AngularJS中的類似的概念,JS中的依賴注入也就是表示,在JS中如果需要調(diào)用某種我們已經(jīng)封裝好的,或者外部的JS庫,那么我們可以提前聲明
    1.在JS中,如果讓JS自己解析,尋找我們定義的變量,這個(gè)過程的效率很低,無形中會(huì)損耗一部分性能,如果我們以實(shí)參的方式傳入,那么會(huì)減少這部分額性能損耗
    (function(window){
    
      })(window)
    2.提前聲明我們使用了什么庫,便于代碼的維護(hù)和可讀性,保障代碼的健壯性
    (function($){
    
      })($)
    
  5. 提前返回

      var addEventlistner=function (){
       if(window.addEventlistner){
    
        return function(elem,type,calback,bool){
    
          elem.addEventlistner("type",calback,bool);
        };
      }else{
          return function(elem,type,calback){
    
            elem.attachEvent("on"+type, calback)
        };
      }
    }();
     //在之前我們進(jìn)行能力檢測(cè)的時(shí)候,每次調(diào)用都會(huì)進(jìn)行if..else..的判斷,其實(shí)在我們進(jìn)入瀏覽器的時(shí)候,瀏覽器的能力已經(jīng)確定了,所以我們可以通過利用閉包的提前返回的特性,把能力檢測(cè)的結(jié)果保存下來,不需要每次都進(jìn)行判斷
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容