JavaScript閉包原理與用法實例

###一、與閉包有關的兩個概念:前端

(1)變量的做用域 不帶有關鍵字var的變量會成爲全局變量; 在函數中使用關鍵字var聲明的變量是局部變量。 局部變量只有在函數內部才能訪問到,在函數外面是訪問不到的。但在函數內部能夠經過做用域鏈一直向上搜索直到全局對象,也就是說,函數內部能夠訪問函數外部的變量。數組

(2)變量的生存週期 對於全局變量,其生存週期是永久的,除非主動銷燬這個全局變量; 而對於在函數內用關鍵字var聲明的局部變量,當退出函數時,這些局部變量會隨着函數調用結束而被銷燬。閉包

var func = function() {
  var i = 1;
  alert(i); // 輸出:1
};
alert(i); // 報錯:i is not defind.

例外狀況:閉包異步

var func = function() {
  var i = 1;
  return function() {
    alert(i);
    i++;
  }
};
var f1 = func();
f1(); // 輸出:1
f1(); // 輸出:2
var f2 = func();
f2(); // 輸出:1
f2(); // 輸出:2

###二、從閉包的一個經典應用談起函數

<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
  var divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    divs[i].onclick = function() {
      alert(i);
    };
  }//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
</script>

問題:不管單擊哪一個div,都會彈出5。 緣由:onclick事件是異步觸發的,當事件被觸發時,for循環早已結束,此時變量i的值早已是5。 解決:在閉包的幫助下,把每次循環的i值都封閉起來。當事件函數順着做用域鏈從內到外查找變量i時,會先找到被封閉在閉包環境的i,單擊div時,會分別輸出0,1,2,3,4。性能

<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
  divs[i].onclick = (function(num) {
    return function() {
      alert(num);
    };
  })(i);
}//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
</script>

相似實例:閉包直接賦給數組學習

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++){
    result[i] = function(){
      return i;
    };
  }
  return result;
}
for (var i = 0; i < 10; i++)
  alert(createFunctions()[i]());

結果:result的每一個元素都返回10。 說明:閉包的做用域鏈有明顯的反作用——閉包老是得到外部函數變量的最終值。上面代碼中,外部函數產生一個函數數組result並返回。函數數組中的每一個元素都是一個函數,每一個函數都返回 i變量。彷佛每一個函數應該返回每次循環的i值,即依次返回0到9,但事實是,每一個函數的返回結果都是10。這是由於每一個內部函數返回的是變量i,而不是i在某個時刻的特定值,而i的做用域是整個外部函數,當外部函數執行完成後,i的值是10。 解決:在每一個內部函數的內部,再產生一個匿名函數並返回。this

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }
  return result;
}//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
for (var i = 0; i < 10; i++)
  alert(createFunctions()[i]());

結果:result依次返回0到9。 說明:(i)使得該層匿名函數當即執行。 ###三、閉包 3d

有時候須要獲得函數內的局部變量。如何從外部讀取局部變量?那就是在函數的內部,再定義一個函數。 閉包是指有權訪問另外一個函數做用域中變量的函數,建立閉包的最多見的方式就是在一個函數內建立另外一個函數,經過另外一個函數訪問這個函數的局部變量,利用閉包能夠突破做用鏈域,將函數內部的變量和方法傳遞到外部。 ① 閉包的原理code

  • 後臺執行環境中,閉包的做用域鏈包含着本身的做用域、函數的做用域和全局做用域。
  • 一般,函數的做用域和變量會在函數執行結束後銷燬。
  • 可是,當函數返回一個閉包時,這個函數的做用域將會一直在內存中保存到閉包不存在爲止。 ② 閉包的特性
  • 函數內再嵌套函數。
  • 內部函數能夠引用外層的參數和變量。
  • 參數和變量不會被垃圾回收機制回收。 ③ 閉包的用途
  • 讀取函數內部的變量。
function f1(){
  var n = 999;
  function f2(){
    alert(n);//999
  }
}//歡迎加入全棧開發交流圈一塊兒學習交流:864305860

在上面的代碼中,函數f2就被包括在函數f1內部,這時f1內部的全部局部變量,對f2都是可見的。可是反過來就不行,f2內部的局部變量,對f1就是不可見的。既然f2能夠讀取f1中的局部變量,那麼只要把f2做爲返回值,就能夠在f1外部讀取它的內部變量了。

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

閉包就是可以讀取其餘函數內部變量的函數。因爲在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。因此,在本質上,閉包就是將函數內部和函數外部鏈接起來的一座橋樑。

function f1(){
  var n = 999;
  nAdd = function(){n += 1}
  function f2(){
    alert(n);
  }//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
  return f2;//面向1-3年前端人員
}//幫助突破技術瓶頸,提高思惟能力
var result=f1();
result();//999
nAdd();
result();//1000

result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證實了,函數f1中的局部變量n一直保存在內存中,並無在f1調用後被自動清除。緣由就在於f1是f2的父函數,而f2被賦給了一個全局變量,這致使f2始終在內存中,而f2的存在依賴於f1,所以f1也始終在內存中,不會在調用結束後,被垃圾回收機制回收。 (2)讓函數內部的變量的值始終保持在內存中(延長局部變量的壽命)。

var print, add, set;
function closure() {
  var number = 8;
  print = function() {
    alert(number);
  }
  add = function() {
    number++;
  }
  set = function(x) {
    number = x;
  }
}
closure();//建立一個閉包
add();
print();//9
set(0);
print();//0
var oldClosure = print;
closure();//建立一個新的閉包
print();//8
oldClosure();//0

因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。也就是說,閉包會引用外部函數做用域,會佔用更多的內存,過分使用閉包,會致使性能問題。因此,僅當必要時才使用閉包。對產生閉包的函數,使用後應該解除引用。 (3)自執行函數+閉包減小全局變量污染(封裝私有變量)

var person = (function() {
  var_name = "Alice";
  var _id = 16;
  return {
    getUserInfo: function() {
      return _name + ": " + _id;
    }//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
  }//面向1-3年前端人員
})();//幫助突破技術瓶頸,提高思惟能力

使用下劃線來約定私有變量_name和_age,它們被封裝在閉包產生的做用域中,外部是訪問不到這兩個變量的,這就避免了對全局的命令污染。 ④ 閉包的缺點: (1) 須要維護額外的做用域。 (2) 過渡使用閉包會佔用大量內存。 四、this對象 在閉包內使用this對象將產生一些複雜的行爲。this對象的值基於函數所在的執行環境在運行時決定:在全局函數中使用時,this等於window(非嚴格模式)或undefined(嚴格模式);而看成爲對象的方法調用時,this等於這個對象。

var name = "The window";
var object = {
  name: "My object",
  getName: function() {
    return function() {
      return this.name;
    };
  }
};
alert(object.getName()());//輸出:"The window"

每一個函數一旦被調用,它將自動得到this和arguments兩個變量。一個內部函數是不能直接從外部函數訪問到這兩個變量的。能夠經過將this對象存儲在另外一個變量中來解決這個問題。把外部做用域中的this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。

var name ="The window";
var object = {
  name: "My object",
  getName: function() {
    var that = this;
    return function() {
      return that.name;
    };//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
  }//面向1-3年前端人員
};//幫助突破技術瓶頸,提高思惟能力
alert(object.getName()());//輸出:"My object"

要讓閉包訪問外部函數的this和arguments對象,能夠經過將它們的引用存儲在另外一個變量中來完成。 五、內存泄漏 使用閉包的時候很容易形成循環引用,若閉包的做用域包含着一些DOM節點,這時候就有可能形成內存泄漏,但其實,這自己不是閉包的問題,而是因爲:BOM和DOM中的對象是使用C++以COM對象的方式實現的,而COM對象的垃圾收集機制採用的是引用計數策略,在基於引用計數策略的垃圾回收機制中,若兩個對象之間造成了循環引用,則這兩個對象都沒法被回收。

function assignHandler() {
  var element = document.getElementById("id");
  element.onclick = function() {
    alert(element.id);
  }
}

匿名函數保存了一個對element的引用,只要匿名函數存在,element的引用數至少爲1,它所佔用的內存就永遠不會被回收。

function assignHandler() {
  var element = document.getElementById("id");
  var id = element.id;
  element.onclick = function() {
    alert(id);
  }
  element = null;
}

經過把element.id的一個副本保存在變量中,而且在閉包中引用該變量消除了循環引用,可是僅僅作到這一步還不能解決內存泄漏的問題,閉包會引用包含函數的全部活動對象,包含element,即便閉包不直接引用element,包含函數的活動對象中也仍然會保存一個引用,所以有必要把element元素設置爲null,這樣就能解除對DOM對象的引用,確保正常回收其佔用的內存。 六、模仿塊級做用域 JavaScript中沒有直接的塊級做用域。

function output(count) {
  for (var i = 0; i < count; i++) {
    alert(i);
  }
  alert(i);//輸出:10
}

使用閉包能夠模仿塊級做用域——建立並當即調用一個函數,這樣既能夠執行其中的代碼,又不會在內存中留下對該函數的引用。結果是內部函數的全部變量都會當即被銷燬,除非將某些變量賦值給了包含做用域(即外部做用域)中的變量。 用做塊級做用域的匿名函數:將函數聲明包含在一對圓括號中,表示它其實是一個函數表達式,緊隨其後的另外一對圓括號會當即調用這個函數。

(function() {
  //這裏是塊級做用域;
}) ();

可使用匿名函數表達式來模擬塊級做用域,任何定義在匿名函數中的變量在匿名函數執行完以後都將被銷燬,在匿名函數外訪問這些變量將會產生錯誤。

function output(count) {
  (function() {
    for (var i = 0; i < count; i++) {
      alert(i);
//歡迎加入全棧開發交流圈一塊兒學習交流:864305860
    }
  }) ();
  alert(i);//出錯
}

結語

感謝您的觀看,若有不足之處,歡迎批評指正。

相關文章
相關標籤/搜索