1. 程式人生 > >JavaScript閉包原理與用法例項

JavaScript閉包原理與用法例項

###1、與閉包有關的兩個概念:

(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

###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。
解決:在每個內部函式的內部,再產生一個匿名函式並返回。

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)使得該層匿名函式立即執行。
###3、閉包

有時候需要得到函式內的區域性變數。如何從外部讀取區域性變數?那就是在函式的內部,再定義一個函式。
閉包是指有權訪問另一個函式作用域中變數的函式,建立閉包的最常見的方式就是在一個函式內建立另一個函式,通過另一個函式訪問這個函式的區域性變數,利用閉包可以突破作用鏈域,將函式內部的變數和方法傳遞到外部。
① 閉包的原理

  • 後臺執行環境中,閉包的作用域鏈包含著自己的作用域、函式的作用域和全域性作用域。
  • 通常,函式的作用域和變數會在函式執行結束後銷燬。
  • 但是,當函式返回一個閉包時,這個函式的作用域將會一直在記憶體中儲存到閉包不存在為止。
    ② 閉包的特性
  • 函式內再巢狀函式。
  • 內部函式可以引用外層的引數和變數。
  • 引數和變數不會被垃圾回收機制回收。
    ③ 閉包的用途
  • 讀取函式內部的變數。
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) 過渡使用閉包會佔用大量記憶體。
4、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物件,可以通過將它們的引用儲存在另一個變數中來完成。
5、記憶體洩漏
使用閉包的時候很容易造成迴圈引用,若閉包的作用域包含著一些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物件的引用,確保正常回收其佔用的記憶體。
6、模仿塊級作用域
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);//出錯
}

結語

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

本次給大家推薦一個免費的學習群,裡面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q群:864305860,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視訊資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峰。