JavaScript:閉包學習

weixin_34148456發表於2017-04-07

基本概念

  • 閉包是指有權訪問另一個函式作用域變數的函式,建立閉包的通常方式,是在一個函式內部建立另一個函式
  • 如果一個函式訪問了它的外部變數,那麼它就是一個閉包。
  • 閉包的本質是函式
  • 閉包能訪問其他函式的變數
  • 閉包通常作為其他函式的返回值,也可能是函式的引數
  • 外部函式不能訪問內部函式的變數,但是內部函式可以訪問外部函式的變數。所以閉包通常是被返回的(引數中的)內部函式
  • "鏈式作用域"結構(chain scope):子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。
  • 自由變數跨作用域取值時:要去建立這個函式的作用域取值,而不是“父作用域”。
  • 函式的特別之處在於可以建立一個獨立的作用域。
  • 閉包引用的變數,儲存在建立閉包的函式的作用域中,可以一直延伸到全域性作用域,形成作用域鏈(chain scope)。
  • this的關鍵是確定呼叫函式的物件;閉包變數的關鍵確定建立閉包的作用域鏈。這兩者有本質的區別。
var a = 10;
var b = 200;

function fn() {
    var b = 20;

    function bar() {
        console.log("a + b = " + (a + b));
        console.log("this.b = " + this.b);
    }

    return bar;
}

var foo = fn();
foo();  
// a + b = 30
// this.b = 200
  • 閉包foo()的建立環境是作用域鏈bar() => fn() => 全域性
    abar()fn()中都沒有,所以取了全域性的a = 10
    bbar()中沒有,取了fn()b = 20;;全域性中的 b = 200;被遮蔽了。
  • 閉包foo()的呼叫者(執行環境)是全域性,所以this.b指的是全域性中的 b = 200;

使用場景

閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函式內部的變數,另一個就是讓這些變數的值始終保持在記憶體中。

實現getset

function Person(){  
    var name = "default";     
     
    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
};  
   
var john = Person();  
john.getName();          // "default"
john.setName("john");  
john.getName();          // "john"
console.log(john.name);  // undefined
   
var jack = Person();  
jack.getName();          // "default"
jack.setName("jack");    
jack.getName();          // "jack"
console.log(jack.name);  // undefined 

這個看上去就很物件導向了。封裝了資料name,同時提供了getName()setName(newName)兩個閉包(返回的內部函式)來作為這個變數的訪問介面。

快取

var CachedSearchBox = (function(){  
    var cache = {},  
       count = [];  
    return {  
       attachSearchBox : function(dsid){  
           if(dsid in cache){//如果結果在快取中  
              return cache[dsid];//直接返回快取中的物件  
           }  
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建  
           cache[dsid] = fsb;//更新快取  
           if(count.length > 100){//保正快取的大小<=100  
              delete cache[count.shift()];  
           }  
           return fsb;        
       },  
   
       clearSearchBox : function(dsid){  
           if(dsid in cache){  
              cache[dsid].clearSelection();    
           }  
       }  
    };  
})();  
   
CachedSearchBox.attachSearchBox("input1"); 
  • 閉包attachSearchBox(dsid)clearSearchBox(dsid)訪問了物件cache(也可以看做是字典),所以cache會一直儲存在記憶體中。
  • 這是一個快取,如果命中了,直接就從快取中取,不需要再查詢,可以提升查詢速度。
  • 這是一個利用閉包占記憶體的例子,大多數情況下,要注意閉包導致記憶體佔用過多的問題

解決this問題

var name = "The Window";
var object = {
    name : "My Object",
   getNameFunc : function(){
       return function(){
        return this.name;
     };
   }
};
object.getNameFunc()();  // "The Window"
  • object.getNameFunc()返回的是一個匿名函式,可以給個名字,比如var foo = object.getNameFunc();
  • 然後執行foo();這裡的執行環境是全域性,所以此時的this.name就返回全域性的var name = "The Window";

that大法:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    var that = this;
    return function(){
      return that.name;
    };
  }
};
object.getNameFunc()(); // "My Object"
  • 同樣給匿名函式一個名字,方便分析var foo = object.getNameFunc();這時的執行環境是object,所以函式getNameFunc()中的this指的是object;
  • var that = this; 那麼 that = object; 這樣object中的資訊(比如name : "My Object")都儲存在that中了。that處於函式getNameFunc()的作用域中
  • 函式foo()執行環境是全域性,但是建立環境是函式getNameFunc()
  • 函式foo()訪問了不在自己作用域中的變數that,所以他是一個閉包。
  • 閉包foo()沿著作用域鏈往上找,在函式getNameFunc()的作用域中找到了變數that,就拿出來用了that.name = "My Object"

匿名包裝器

其實就是我們通常說的自執行匿名函式
匿名函式的作用是減少臨時的中間變數,解決取名困難問題。
自執行就是定義後馬上執行,並且只執行一次。如果沒有閉包,函式馬上就垃圾回收了,如果有閉包引用變數,那麼作用域會繼續替閉包儲存變數,相當於一個快取。

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
} // 輸出10個10,與預期有差距
  • 函式setTimeout(fn, time);的引數fn是一個函式;這裡的fn是一個匿名函式,並且使用了外部的變數i,所以這個匿名函式是一個閉包。
  • fn的執行環境是全域性的(非同步執行函式),建立的作用域是for迴圈所在的作用域。
  • fn執行的時候,for迴圈所在的作用域儲存了變數i,閉包可以訪問,由於1秒鐘之後,for迴圈早就執行完了(對電腦來說1秒鐘是很長的時間),這時變數i的值已經變成了10
  • 1秒鐘之後,fn執行了10次,變數i只有1個,值是10,所以輸出了1010
for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}
  • (funtion(e))(i)是一個自執行匿名函式,定義完後馬上執行。變數i屬於for迴圈所在的作用域,通過引數e傳遞給函式setTimeout(fn, time);
  • 每次for迴圈都會建立一個自執行匿名函式(funtion(e))(i),定義完後馬上執行。所以有10個不同的e
  • 自執行匿名函式(funtion(e))(i)雖然執行完了,但是函式作用域還在記憶體中,沒有被回收,因為變數esetTimeout(fn, time);中的閉包使用
  • fn的執行環境是全域性的(非同步執行函式),執行時,取10個不同的自執行匿名函式(funtion(e))(i)中的e來執行
  • 10個不同的e中儲存了0~9,輸出符合預期
  • 10fn執行完後,自執行匿名函式(funtion(e))(i)中的e沒有閉包使用,這些執行匿名函式被系統回收,記憶體佔用下降。

參考文章

深入理解javascript原型和閉包(14)——從【自由變數】到【作用域鏈】
深入理解javascript原型和閉包(15)——閉包
作用域的圖畫得不錯

學習Javascript閉包(Closure)
兩個作用總結得不錯
最後的例子給的挺好,thisthat(或者self)大法

詳解js中的閉包
這裡的圖畫得挺不錯的

js閉包的用途
使用的例子不錯

閉包和引用
迴圈的例子給得不錯

Javascript閉包——懂不懂由你,反正我是懂了
既然是翻譯stackflow的文章,可以看看

相關文章