JS 基礎篇--閉包和引用

簡介

Javascript 中一個最重要的特性就是閉包的使用。由於閉包的使用,當前做用域總能夠訪問外部的做用域。由於Javascript 沒有塊級做用域,只有函數做用域,因此閉包的使用與函數是緊密相關的。javascript

各類專業文獻上的"閉包"(closure)定義很是抽象,很難看懂。個人理解是,閉包就是可以讀取其餘函數內部變量的函數html

因爲在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。java

閉包最大用處有兩個,一個是能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。git

以下例子:github

function f1(){
  var n=999;
  nAdd=function(){n+=1}
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000

在這段代碼中,result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證實了,函數f1中的局部變量n一直保存在內存中,並無在f1調用後被自動清除。網絡

爲何會這樣呢?緣由就在於f1是f2的父函數,而f2被賦給了一個全局變量,這致使f2始終在內存中,而f2的存在依賴於f1,所以f1也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。閉包

這段代碼中另外一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,所以nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數自己也是一個閉包,因此nAdd至關因而一個setter,能夠在函數外部對函數內部的局部變量進行操做。ide

模擬私有變量

function Counter(start) {
    var count = start;
    return {
        increment: function() {
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
foo.get(); // 5

這裏 Counter 返回兩個閉包:函數 increment 和 get。這兩個函數一直保持着對 Counter 做用域的訪問,所以它們能一直訪問到定義在 Counter 做用域的變量 count。函數

私有變量的工做機制

因爲 Javascript 不能夠對做用域賦值和引用,因此在上例中,是沒有辦法在外部直接訪問內部私有變量 count。惟一的方法就是經過定義閉包來訪問。性能

var foo = new Counter(4);
foo.hack = function() {
    count = 1337;
};

上面的代碼不會改變 Counter 做用域內的 count 變量值,由於 hack 沒有在 Counter 內定義。上面這段代碼只會建立或者覆蓋全局變量 count。

循環內的閉包

一個最容易犯的錯誤就是在循環內使用閉包。

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);  
    }, 1000);
}

上面這段代碼不會輸出0到9,而是連續輸出10次10。
上面的匿名會一直保持一個對變量 i 的引用。當調用 console.log 函數開始輸出時,這是循環已經結束,而變量 i 已經爲10了。
爲了不上面的錯誤發生,咱們須要在每次循環時爲變量 i 值建立一個拷貝。

避免引用錯誤

爲了複製循環中變量的值,最好的方式是在外層加一個匿名的馬上執行函數。

for(var i = 0; i < 10; i++) {
    (function(e) {
        setTimeout(function() {
            console.log(e);  
        }, 1000);
    })(i);
}

這個外部的匿名函數接收循環變量 i 做爲第一個參數,並將其值拷貝至它自身的參數 e。
外部的匿名函數將參數 e 再傳遞給 setTimeout,所以 setTimeout 有了指向參數 e 的引用。並且這個參數 e 的值不會由於外部的循環改變而改變。

還有另一個方法能夠實現一樣的效果,就是在 setTimeout 內的匿名函數中再返回一個匿名函數:

for(var i = 0; i < 10; i++) {
    setTimeout((function(e) {
        return function() {
            console.log(e);
        }
    })(i), 1000)
}

此外,經過 bind 方法也能夠實現。

for(var i = 0; i < 10; i++) {
    setTimeout(console.log.bind(console, i), 1000);
}

閉包的注意點

1)因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。

2)閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。

思考題

若是你能理解下面兩段代碼的運行結果,應該就算理解閉包的運行機制了。

代碼片斷一。

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

代碼片斷二。

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

參考地址

Closures and References
阮一峯的網絡日誌:學習Javascript閉包(Closure)

相關文章
相關標籤/搜索