一篇文章帶你理解閉包

走在前端的大道上javascript

本篇將本身讀過的相關 javascript閉包 文章中,對本身有啓發的章節片斷總結在這(會對原文進行刪改),會不斷豐富提煉總結更新。html

首先着重回顧一下 全局變量 和 局部變量

全局變量:能夠在任意位置訪問的量就叫 全局變量

 

var age = 20;
 function a(){
     console.log(age); //20
}
a();

局部變量:函數中用var定義的變量,只能在函數中訪問這個變量,函數外部訪問不了。

function a(){
    var age = 20;
}
a();
console.log(age); // Uncaught ReferenceError: age is not defined

重點:前端

1. 在函數中若是不使用var定義變量那麼js引擎會自動添加成全局變量。
2. 全局變量從建立的那一刻起就會一直保存在內存中,除非你關閉這個頁面,局部變量當函數運行完之後就會銷燬這個變量,假若有屢次調用這個函數它下一次調用的時候又會從新建立那個變量,既運行完就銷燬,回到最初的狀態,簡單來講局部變量是一次性的,用完就扔,下次要我再從新建立。

函數的相關知識點:

1.一個函數內能夠嵌套多個函數
2.函數裏面的子函數能夠訪問它上級定義的變量,注意不僅是一級,若是上級沒有會繼續往上級找,直到找到爲止,若是找到全局變量到找不到就會報錯。java

function a(){
 var name = "追夢子";
 function b(){
     console.log(name); // "追夢子"
 }
 b();
}
a();

3.函數的另一種調用形式,你能夠把它叫作自調用,本身調用本身,達到自執行的效果。閉包

var a = 0;
(function(){
   console.log(++a); // 1
})()

這種方式用()把內容包裹起來,後面的()表示執行這個函數,可能你會問爲何要把函數包起來,若是不包裹起來,js會把它看成函數聲明來處理,若是包裹起來就是表達式。函數

正題

閉包說得通熟易懂一點,就是指 有權訪問另外一個函數做用域變量的函數。閉包能夠解決函數外部沒法訪問函數內部變量的問題。建立閉包的常見方式,就是在一個函數內部建立另一個函數,並返回。

下面是一段沒有使用閉包的代碼:學習

function fn(){
 var a = 10; //報錯
}
alert(a);

由於a沒有定義,雖然函數fn裏面定義了a可是,可是它只能在函數fn中使用。也就是做用域的問題。this

function fn(){
  //定義了一個變量name
 var name = '追夢子';
  //外部想訪問這個變量name怎麼辦?return!把它返回出去,再用個變量接收一下不就能夠了
  return name;
}
var name = fn();//接收fn返回的name值。
alert(name);//追夢子;

這裏的閉包就是利用函數的return。除了經過return還能夠經過其餘的幾種方法以下:
方法1:設計

function fn(){
  var a = 0;
  b = a;
}
alert(b)

 這裏利用了js的一個特性,若是在函數中沒有用var定義變量,那麼這個變量屬於全局的,但這種方法多少有些很差。調試

方法2:

var f = null;
function fn(){
  var a = 0;
  f = function(){
    a++;
    f.a = a;
  };
}
fn();
f();
alert(f.a);//1
f();
alert(f.a);//2

其實閉包還有一個很重要的特性,來看一個例子。

var lis= document.getElementsByTagName['li']; 
 //假如這段代碼中的lis.length = 5;
for(var i=0;i<lis.length;i++){
 lis[i].onclick = function(){
  alert(i);
 };
}

最終結果是無論單擊哪一個li元素都是彈5。不信你試試。爲何呢。看下面分析。

for(var i=0;i<lis.length;i++){ }
      // i = 5對吧
  lis[0].onclick = function(){
    alert(i); 
  };
  lis[1].onclick = function(){
    alert(i); 
  };
  lis[2].onclick = function(){
    alert(i);
  };
  lis[3].onclick = function(){
    alert(i);
  };
  lis[4].onclick = function(){
    alert(i);
  };

爲何會這樣呢,由於你for循環只是給li綁定事件,可是裏面的函數代碼並不會執行啊,這個執行是在你點擊的時候才執行的好吧?可是此時的i已是5了,因此全部的都打印出5來了。

for(var i=0;i<lis.length;i++){ 
   (function(i){
      lis[i].onclick = function(){
         console.log(i); //點擊第幾個返回第幾個
      }
   })(i)
}

閉包的特色不僅是讓函數外部訪問函數內部變量這麼簡單,還有一個大的特色就是 經過閉包可讓函數中的變量持久保持

function fn(){
   var num = 5;
   num+=1;
   alert(num);
}  
 fn(); //6
 fn(); //6

爲何都是 6 呢?由於 函數一旦調用裏面的內容就會被銷燬,下一次調用又是一個新的函數,和上一個調用的不相關了。

劃重點:JavaScript中有回收機制,函數沒有被引用 且執行完之後這個函數的做用域就會被銷燬,若是一個函數被其餘變量引用,這個函數的做用域將不會被銷燬,(簡單來講就是函數裏面的變量會被保存下來,你能夠理解成全局變量。)

再來

function fn(){
 var num = 0;
 return function(){
   num+=1;
     alert(num);   
   };  
}
var f = fn();
f(); //1
f(); //2

定義了一個fn函數,裏面有個num默認爲0,接着返回了一個匿名函數(也就是沒有名字的函數)。咱們在外部用 f 接收這個返回的函數。這個匿名函數乾的事情就是把 num 加 1,還有咱們用來調試的 alert 。

這裏之因此執行完這個函數 num 沒有被銷燬,是由於那個匿名函數的問題,由於這個匿名函數用到了這個 num,因此沒有被銷燬,一直保持在內存中,所以咱們 f() 時 num 能夠一直加。

function a(){
    var aa = 0;
    function b(){
        aa ++;
        console.log(aa);
    }
    return b;
}
var ab = a();
ab(); //1
ab(); //2

裏面的變量的值沒有被銷燬,由於函數a被外部的變量ab引用,因此變量aa沒有被回收。

若是某個函數被它的父函數以外的一個變量引用,就造成了一個閉包

還有一種更爲經常使用的閉包寫法

var bi = (function(){
    var a = 0;
    function b(){
        a ++;
        console.log(a);
    }
    return b;
})();

bi(); //1
bi(); //2
bi(); //3

執行過程分析:

  首先把一個自執行函數賦值給了bi,這個自執行函數運行完成之後就bi的值就變成了

function b(){
    a ++;
    console.log(a);
}

  由於咱們在上面的代碼 return 回去了 b,而後由於這個自執行函數被 bi 引用因此裏面的變量 a 並無由於這個自執行函數執完而銷燬,而是保存到了內存中,因此咱們屢次打印 bi()
就成了一、二、3

閉包是使用能夠帶來如下好處:

  1. 但願一個變量長期駐紮在內存中
  2. 避免全局變量的污染
  3. 私有成員的存在

閉包能夠讀取到函數內部的變量,這是因爲閉包後函數的堆棧不會釋放,也就是說這些值始終保持在內存中。這是一個優勢,也是一個缺點。

經過例子再來回顧一下

閉包的理解:
  所謂的閉包就是能夠建立一個獨立的環境,每一個閉包裏面的環境都是獨立的,互不干擾。
閉包的建立:
  一個函數中嵌套另一個函數,而且將這個函數return出去,而後將這個return出來的函數保存到了一個變量中,那麼就建立了一個閉包。
var arr = [];
for(var i=0;i<2;i++){
    arr[i] = function(){
        console.log(i);
    }
}
arr[0](); //2
arr[1](); //2

  實際狀況咱們是要打印0,1,2,3這樣的數,可是每次都是打印2,什麼狀況呢?雖然咱們在for中給arr的每一個值添加了一個匿名函數,可是 在for循環中咱們並無執行這個函數,而是在for循環之後執行的這個函數,那麼天然打印出來的就是for循環完之後i的值。

var arr = [];
// for(var i=0;i<2;i++){
    var i = 2;
    arr[0] = function(){
        console.log(i);
    }
    arr[1] = function(){
        console.log(i);
    }
// }
arr[0](); //2
arr[1](); //2

  至關於這樣,雖然這個函數沒有執行,可是arr的i已經給執行了,由於arr不是函數啊,確定是執行的,而你函數沒有調用天然就不會執行,當函數調用的時候i已是2了。既然如此只要咱們在for循環的時候直接執行這個函數就ok。

var arr = [];
for(var i=0;i<2;i++){
    arr[i] = function(){
        console.log(i);
    }
    arr[i]();
}
//0
//1

 這樣,在每一次for循環的時候就直接執行了這個函數,打印正常,可是咱們這樣一樣有一個問題,那就是在每一次for循環的時候這個函數就已經被執行了,咱們要的是咱們想何時調用就時候調用,而不是直接在for執行中直接執行,那麼顯然這樣作是達不到咱們的目的的。

  如今咱們在想一想閉包的概念,閉包能夠建立獨立的環境,而且每個閉包的環境是獨立的,也就是說,咱們能夠經過閉包來保存這些不一樣的變量。

咱們回顧一下閉包的建立方法:一個函數中嵌套另一個函數,而且將這個函數return出去,而後將這個return出來的函數保存到了一個變量中,那麼就建立了一個閉包

var arr = [];
for(var i=0;i<2;i++){
    arr[i] = a(i);
}

function a(i){
    return function(){
        console.log(i);
    }
}

arr[0](); //0
arr[1](); //1

   此時就是一個閉包,這樣寫有些麻煩,咱們對此改進一下。

var arr = [];
for(var i=0;i<3;i++){
    arr[i] = (function(i){
        return function(){
            console.log(i);
        }
    })(i)
}

arr[0](); //0
arr[1](); //1
arr[2](); //2

還能夠這樣

var arr = [];
for(var i=0;i<3;i++){
    (function(i){
        arr[i] = function(){
            console.log(i);
        }
    })(i)
}

arr[0](); //0
arr[1](); //1
arr[2](); //2

 此時 arr 裏面的 i 用的是閉包裏面的 i,而不是 for 中的 i,由於咱們說過每一個閉包的環境都是獨立的。

js中的回收機制

function a(){
  var num = 10;
  return function(){
    num ++;
    console.log(num);
  }
}
a()(); //11
a()(); //11

按理說第二次執行函數a的時候應該打印出12纔對,可是打印的倒是11。

首先來看看咱們的理解

1.咱們在函數a中返回了一個匿名函數,在這個匿名函數中咱們num++了一下,而後咱們在函數外面執行了這個匿名函數函數,(第一括號執行函數a第二個括號執行這個rutrun回去的函數)
2.如今num是11,而後咱們又執行了一次這個函數,大家應該是12吧,爲何不是呢?

實際js的執行

  可是js的設計者爲了讓沒有必要的變量保存在內存中,(咱們寫的任何變量都是須要內存空間的),什麼叫沒有必要的變量?也就是說你不在須要這個變量的時候它就會被銷燬?那麼你確定會問js怎麼知道那些變量是咱們不須要的哪些是咱們須要的。因此js爲了知道哪些變量須要保存下來,哪些不須要保存下來,會進行一些判斷。接下來咱們就一塊兒看看js是怎麼判斷的。

1.在js中定義的全局變量是不會被銷燬的,由於咱們隨時均可能會用到這個變量,因此不能被銷燬。

2.可是在函數中定義的變量就不必定了,並且因爲在函數的定義的變量的生命週期在執行完這個函數就銷燬的緣由天然就保存不了上一次的值。

3.可是並非說函數就真的保存不了上一次的值,由於有的時候咱們確實須要上一次的值,因此js判斷是否須要保存上一次變量的值的時候就會遵照這樣一個規則:

若是這個函數有被外部的變量引用就不會銷燬(這句話說的不夠準確,下面代碼會一步一步解釋),不然銷燬。怎麼理解這句話呢?

function a(){
    var b = 0;
    return function(){
        b ++;
        console.log(b);
    }
}

var d = a();
d();//1
d();//2

  函數a被變量d引用,更準確的說是函數a裏面的那個匿名函數被變量d所引用,由於變量d等於的是函數a執行完成後的值,而函數a執行完之後又由於函數a返回了那個匿名函數,因此準確的說是變量d等於匿名函數。而這個匿名函數由於使用了函數a中的變量b而且還被變量d所引用,因此就造成了一個閉包,只要這個變量d不等於null的話,那麼那個變量b會一直保存到變量d中不會被銷燬。

總結:

一、若是一個對象不被引用,那麼這個對象就會被GC回收;
二、若是兩個對象互相引用,可是沒有被第3個對象所引用,那麼這兩個互相引用的對象也會被回收。

參考文章:
1.初識js中的閉包
2.從閉包案例中學習閉包的做用,會不會由你
3.那些年咱們一塊兒過的JS閉包,做用域,this,讓咱們一塊兒劃上完美的句號
4.再次講解js中的回收機制是怎麼一回事。

相關文章
相關標籤/搜索