分享

【原】理解javascript中的閉包

 ipilipala 2016-11-21

閉包在javascript來說是比較重要的概念,平時工作中也是用的比較多的一項技術。下來對其進行一個小小的總結

 

什麼是閉包?

 

官方說法:

  閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數,通過另一個函數訪問這個函數的局部變量------《javascript高級程序設計第三版》

 

下面就是一個簡單的閉包:

function A(){
    var text="hello world";
    function B(){
       console.log(text);
    }
    return B;
}
var c=A();
c(); // hello world 

按照字面量的意思是:函數B有權訪問函數A作用域中的變量(text),通過另一個函數C來訪問這個函數的局部變量text。因此函數B形成了一個閉包。也可以說C是一個閉包,因為C執行的實際是函數B。

這個需要注意的是,直接執行A();是沒有任何反應的。因為return B沒有執行,除非是return B();

閉包的特性

 

 閉包有三個特性:

 1.函數嵌套函數
2.函數內部可以引用外部的參數和變量
3.參數和變量不會被垃圾回收機制回收

解釋一下第3點,為什麼閉包的參數和變量不會被垃圾回收機制回收呢?

首先我們先瞭解一下javascript的垃圾回收原理:

(1)、在javascript中,如果一個對象不再被引用,那麼這個對象就會被GC(garbage collection)回收; 

(2)、如果兩個對象互相引用,而不再被第3者所引用,那麼這兩個互相引用的對象也會被回收。

  上面的示例代碼中A是B的父函數,而B被賦給了一個全局變量C(全局變量的生命週期直至瀏覽器卸載頁面才會結束),這導致B始終在內存中,而B的存在依賴於A,因此A也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。

閉包的作用:

 

  其實閉包的作用也是有閉包的特性決定的,根據上面的閉包特性,閉包的作用如下:

  1、可以讀取函數內部的變量,而不是定義一起全局變量,避免污染環境

  2、讓這些變量的值始終保持在內存中。

閉包的代碼示例

 

下面主要介紹幾種常見的閉包,並進行解析:

 

demo1 局部變量的累加。

1
2
3
4
5
6
7
8
9
10
11
12
function countFn(){
    var count=1;
    return function(){             //函數嵌套函數
        count++;
        console.log(count);
    }
}
var y = countFn();    //外部函數賦給變量y;
y();   //2      //y函數調用一次,結果為2,相當於countFn()()
y();   //3     //y函數調用第二次,結果為3,因為上一次調用的count還保存在內存中,沒有被銷毀,所以實現了累加
y=null//垃圾回收,釋放內存
y();  // y is not a function

由於第一次執行完,變量count還保存在內存中,所以不會被回收,以致於第二次執行的時候可以對上次的值就行累加。當引入y=null時,銷毀引用,釋放內存

 

demo2 循環中使用閉包


代碼如下(下面的三個代碼示例):我們的目的是想在每次循環中調用循環序號:

 

demo2-1

for (var i = 0; i < 10; i++) {
    var a = function(){
        console.log(i)
    }
    a()  //依次為0--9
}

這個例子的結果是沒有題的,我們依次打印出了0-9

每一層匿名函數和變量i都組成了一個閉包,但是這樣在循環中並沒有問題,因為函數在循環體中立即被執行了

 

demo2-2

但是在setTimeout中就不一樣了

1
2
3
4
5
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  //10次10
    }, 1000);
}

我們期望的依次是打印出0--10,實際情況是打印出 10次10。即使吧setTimeout的時間改為0,也是打印出10個10。這是為什麼呢?

    這是因為setTimeout的一種機制,setTimeout是從任務隊列結束的時候開始計時的,如果前面有進程沒有結束,那麼它就等到它結束再開始計時。在這裡,任務隊列就是它自己所在的循環。

循環結束setTimeout才開始計時,所以無論如何,setTimeout裡面的i都是最後一次循環的 i。該代碼中,最後的 i 為10,所以打印出了10個10.

    這也就是為什麼setTimeout的回調不是每次取循環時的值,而取最後一次的值

 

demo2-3

解決上面的setTimeout不能依次打印出循環的問題

複製代碼
for(var i=0;i<10;i++){
    var a=function(e){
        return function(){

            console.log(e); //依次輸入0--9
        }
    }
    setTimeout(a(i),0);
}
複製代碼

因為setTimeout第一個參數需要一個函數,所以返回一個函數給它,返回的同時把 i 作為參數傳進去,通過形參 e 緩存了i,也就是說e變量相當於是 i 的一個拷貝 ,並帶進返回的函數裡面。

 setTimeout 的執行時,它就擁有了對 e 的引用,而這個值是不會被循環改變的。

 

也可以用下面的寫法,和上面類似:

複製代碼
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  //依次打印出0-9
        }, 0);
    })(i);
}
複製代碼

 

 demo3 循環中添加事件

 看下面的一個典型的demo.

我們希望每次點擊li的時候,alert出li的索引值,所以用下面的代碼:

複製代碼
 <ul id="test">
    <li>第一個</li>
    <li>第二個</li>
    <li>第三個</li>
    <li>第四個</li>
</ul>

var nodes = document.getElementsByTagName("li");
for(i = 0,len=nodes.length;i<len;i++){
    nodes[i].onclick = function(){
        alert(i);   //值全是4
    };
}
複製代碼

事與願違,無論點擊哪一個li,都是alert(4),也就是都是alert循環結束之後的索引值。這是為什麼呢?

    這是因為循環中為不同的元素綁定事件,事件回調函數裡如果調用了跟循環相關的變量,則這個變量取循環的最後一個值。

    由於綁定的回調函數是一個匿名函數,所以上面的代碼中, 這個匿名函數是一個閉包,攜帶的作用域為外層作用域(也就是for裡面的作用域),當事件觸發的時候,作用域中的變量已經隨著循環走到最後了。

    還有一點就是,事件是需要觸發的,而絕大多數情況下,觸發的時候循環已經結束了,所以循環相關的變量就是最後一次的取值。

 

要實現點擊li,alert出li的索引值,需要將上面的代碼進行以下的修改:

複製代碼
<ul id="test">
    <li>第一個</li>
    <li>第二個</li>
    <li>第三個</li>
    <li>第四個</li>
</ul>
var nodes=document.getElementsByTagName("li");
for(var i=0;i<nodes.length;i++){
    (function(e){
        nodes[i].onclick=function(){
            alert(e);
        };
    })(i)
}
複製代碼

   解決思路: 增加若干個對應的閉包域空間(這裡採用的是匿名函數),專門用來存儲原先需要引用的內容(下標)。

   當立即執行函數執行的時候,e 值不會被銷毀,因為它的裡面有個匿名函數(也可以說是因為閉包的存在,所以變量不會被銷毀)。執行後,e 值 與全局變量 i 的聯繫就切斷了,

也就是說,執行的時候,傳進的 i 是多少,立即執行函數的 e 就是多少,但是 e 值不會消失,因為匿名函數的存在。 

 

也可以用下面的解法,原理是一樣的:

複製代碼
<ul id="test">
    <li>第一個</li>
    <li>第二個</li>
    <li>第三個</li>
    <li>第四個</li>
</ul>

var nodes=document.getElementsByTagName('li');
for(var i = 0; i<nodes.length;i++){
    (function(){
       var temp = i;
        nodes[i].onclick = function () {
            alert(temp);
        }
    })();
}
複製代碼

 

 

注意事項

 

  1、造成內存洩露

    由於閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的內存。過度使用閉包可能會導致內存佔用過多,所以只有在絕對必要時再考慮使用閉包。

  2、在閉包中使用this也可能會導致一些問題。

       代碼示例:來源於《js高級程序設計3》;

其實我們的目的是想alert出object裡面的name

複製代碼
 var name="The Window";
 var object={
     name:"My Object",
     getNameFunc:function(){
         return function(){
             return this.name;
         }
     }
 }
 alert(object.getNameFunc()()); // The Window
複製代碼

 因為在全局函數中,this等於window,而當函數被作為某個對象的方法調用時,this等於那個對象。不過,匿名函數的執行環境具有全局性,因此其this對象通常指向window。

每個函數在被調用時,都會自動取的兩個特殊變量:this和 arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止。也就是說,裡面的return function只會搜索

到全局的this就停止繼續搜索了。因為它永遠不可能直接訪問外部函數中的這兩個變量。

 

稍作修改,把外部作用域中的this對象保存在一個閉包能夠訪問的變量裡。這樣就可以讓閉包訪問該對象了。

複製代碼
 var name="The Window";
 var object={
     name:"My Object",
     getNameFunc:function(){
         var that=this;
         return function(){
             return that.name;
         }
     }
 }
 alert(object.getNameFunc()()); // My Object
複製代碼

我們把this對象賦值給了that變量。定義了閉包之後閉包也可以訪問這個變量。因此,即使在函數返回之後,that也仍引用這object,所以調用object.getNameFunc()()就返回 「My Object」了。

  

總結

 

當在函數內部定義了其他函數,就創建了閉包。閉包有權訪問包含函數內部的所有變量。

  閉包的作用域包含著它自己的作用域、包含函數的作用域和全局作用域。

  當函數返回一個閉包時,這個函數的作用域會一直在內存中保存到閉包不存在為止。

  使用閉包必須維護額外的作用域,所有過度使用它們可能會佔用大量的內存

 

有誤之處,歡迎指出

如果您覺得文章有用,可以打賞個咖啡錢

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多