先說句題外話,如果你覺得我講的明白的話給個反饋,點個贊,留個言啥的。為啥js中有閉包這個東西,其他後臺語言裏沒有?當一個函數調用時,內部變數的查找會按作用域鏈條來查找,按理說不會有啥特殊情況出現,js之所以會出現閉包這個現象,原因就是你調用的這個函數是另外一個函數的返回值。說到函數能作為返回值,這是跟js中函數類型是第一類對象這種語言設計方式有關,我會先介紹第一類對象對js的編碼風格的影響。當然你直接可以看第二部分,關於閉包的說明。第一部分啥是第一類對象呢,我幫你百度了。第一類對象不一定是面向對象程序設計所指的物件,而可以指任何程序中的實體。一般第一類對象所特有的特性為:可以被存入變數或其他結構可以被作為參數傳遞給其他函數可以被作為函數的返回值可以在執行期創造,而無需完全在設計期全部寫出即使沒有被繫結至某一名稱,也可以存在在js中object類型就是第一類對象。你能怎麼使用object類型。就怎麼能使用function類型。這裡先說一下js中函數的四大作用1.可以調用執行。程序世界裡,函數的基本作用就是可復用代碼段的封裝,可以直接調用。2.可以按照對象的使用方式來使用。原因就是js中函數類型是第一類對象。準確的來說js中函數本身就是對象。對象能做啥,他當然也能做。3.可以提供作用域。js中沒有塊級作用域的概念。函數是提供作用域的最小單位。4.可以作為構造函數。這一作用算是函數的特殊作用。可以作用生成其他對象的模板。也就是可以通過函數來模擬類的實現。在講閉包之前,先大致對函數的使用方式與對象和數組(數組本來就是對象,當然函數也是)做個對比。1.關於字面量對象的字面量"{}"

  1. var a = new Object();
  2. a.b = "xxx";
  3. //等同於
  4. var a = {};
  5. a.b = "xxx";
  6. //或者
  7. var a = {
  8. b : "xxx"
  9. };

複製代碼數組的字面量"[]"

  1. var a = new Array("a","b","c");
  2. //等同於
  3. var a = ["a","b","c"];

複製代碼那麼函數呢,我們平常聲明函數的方式,可以理解成是一種字面量(我沒有說是)

  1. function a(x,y){
  2. return x + y;
  3. }
  4. //相當於如下對象的字面量
  5. var a = new Function(x,y,"return x + y;");

複製代碼注意:上面所有a 都是對象的引用2.關於匿名函數同樣也有匿名對象和匿名數組,我們先看看他們是怎麼使用的

  1. var b = ({
  2. a : "xxx"
  3. }).a
  4. alert(b) // "xxx"
  5. for(var i = 0; i < [1,2,3].length; i++){
  6. console.log(i);
  7. }

複製代碼同樣函數也有匿名的

  1. funcion(){
  2. alert("11");
  3. }
  4. //因為函數的最基本功能是調用,匿名函數也可以調用(我習慣稱呼為函數自執行,一般書上都叫函數立即調用表達式)
  5. (function(){alert(11)})();

複製代碼3.可以存進變數或者其他結構。因為數組元素中可以存入數組,當然也可以存入函數。對象也是,鍵值對的值可以存入任何東西,當然也可以存入函數,這時我們一般都用匿名函數,例如

  1. var a = function(){};//這種聲明函數的方式也叫函數直接量。
  2. var a = {
  3. say : function(){//....}
  4. };

複製代碼4.可以做為參數,傳入函數也就是平常我們說的回調函數。眾所周知函數有參數和返回值對象和數組作為參數沒得說,寫下函數相關的例子

  1. var a = function(b){
  2. b();
  3. };
  4. // 可以傳入匿名函數,jquery中各種回調都是匿名的
  5. a(function(){alert("123");});
  6. //傳入有名字的函數,跟c聲明的位置無關,這裡涉及到變數提升的問題以及函數優先初始化的問題。
  7. a(c);
  8. function c(){
  9. alert("222")
  10. }

複製代碼廣義的講,當然了,回調函數,傳入參數不一定非得函數變數,但是一定要包含函數的結構(例如數組、object對象、自定義對象),如下

  1. var a = function(object)
  2. object.say();
  3. }
  4. a({x :"2222",say :function(){alert("xxxx")}});

複製代碼5.作為返回值對象和數組作為函數的返回值沒得說,寫下函數相關的例子

  1. function a(){
  2. return function(){
  3. alert("22222");
  4. };
  5. }
  6. (a())();//alert "22222";

複製代碼

第二部分現在還是說說為啥出了個閉包這個東西,原因就是你調用的那個函數是另一個函數的返回值,當外部調用時,會沿著這個返回值的函數作用域鏈條來找其內部相關變數的。先大致說下作用域鏈條的問題。函數中識別變數,是一層層向外找的,首先在函數內部找,看看是不是內部聲明的,然後再到上一層找,沒找到,再往上,直到全局作用域。如果全局頁面都沒聲明,那瀏覽器就報錯了。這一層層中的層是什麼東西呢,就是函數,因為函數提供最小的作用域。看個例子

  1. var a = 3;
  2. var b = 4;
  3. function outer(){
  4. var a = 5;
  5. var c = 7;
  6. var d = 8;
  7. console.log(a);//5,outer內部的
  8. console.log(b);//4,全局的
  9. var inner = function(){
  10. var c= 6;
  11. console.log(b);//4,全局的
  12. console.log(c);//6,inner內部的
  13. console.log(d);//8,outer內部的
  14. //console.log(e); //報錯,沒找到
  15. b = 0 //找到全局的
  16. d = "xxx";
  17. }
  18. inner();
  19. console.log(b);//0,找全局的b
  20. console.log(d);//"xxx", outer內部的
  21. }
  22. outer();

複製代碼作用域鏈條我們明白了,然後咱再來看看閉包的情形

  1. //代碼1
  2. function a(){
  3. var x = 0;
  4. return function(){
  5. x++;//此函數的作用域鏈能看到x
  6. console.log(x);
  7. }
  8. }
  9. var fun = a();//a返回的是個函數,保存起來沒問題。
  10. fun()//列印1
  11. fun()//列印2

複製代碼為啥列印2而不是1呢,原因是因為a中返回個函數,我們要調用這個函數,瀏覽器一看,你要運行的是函數,函數是有作用域鏈條的,哦,x我能找到,保證不報錯的。裡面的x當然也能自增加了說的直白點就像如下代碼一樣

  1. //代碼2
  2. var x = 0;
  3. var fun = function(){
  4. x++;
  5. console.log(x);
  6. }
  7. fun();//列印1
  8. fun();//列印2

複製代碼補充:經網友提醒,閉包有佔用內存的問題,這裡說下,因為代碼1中fun是一個函數的引用,瀏覽器對應的會對其作用域鏈條中的變數x做了保存,因而會佔用內存。達到的效果就跟代碼2中的x一樣。要釋放其內存可以把其引用置空,使a返回的那個匿名函數無引用指向它,自然垃圾回收器會回收的。代碼如下

  1. //代碼3
  2. function a(){
  3. var x = 0;
  4. return function(){
  5. x++;//此函數的作用域鏈能看到x
  6. console.log(x);
  7. }
  8. }
  9. var fun = a();//a返回的是個函數,保存起來沒問題。
  10. fun()//列印1
  11. fun()//列印2
  12. //以後不再使用了,注意要釋放內存
  13. fun = null;

複製代碼注意:如果我換種調用方法呢(a())();//列印1(a())();//列印1誒,為啥第二次不列印2了呢。原因很簡單,因為兩次調用返回的不是同一個函數引用,因此是兩條作用域鏈條。說的直白點就像如下的代碼

  1. var x1 = 0;
  2. (function(){
  3. x1++;
  4. console.log(x1);
  5. })();//列印1
  6. var x2 = 0;
  7. (function(){
  8. x2++;
  9. console.log(x2);
  10. })();//列印1

複製代碼這種使用方式,就不會有出現閉包常駐內存的情況,因為每次使用都匿名的,當然了,也失去了閉包的意義。大體閉包這種現象我是解釋明白了。我沒有給閉包下明確的定義,不同的書有不同的說法。有的說,返回的那個函數是閉包,有的說返回的函數提供的作用域鏈條是閉包。有的甚至把其得到效果說是閉包,大體是這麼說的,通過這種方式,能訪問某個部函數內部的私有變數,這種方式稱為閉包。不管怎麼說都是跟函數的作用域鏈條相關的。更有甚者也有說所有函數都是閉包。我個人覺得會出現閉包這個東西,主要原因就是跟js中函數是第一類對象有關,因為你調用的一個函數可能不是直接聲明的,而是其他函數直接return的函數或者return某種結構中的一個函數。關於是返回某種結構的中函數,舉例如下

  1. function a(){
  2. var x = 0;
  3. var y = {name :"張三"};
  4. var f1 = function(){
  5. x ++;
  6. }
  7. var f2 = function(name){
  8. y.name = name;
  9. }
  10. return [f1,f2];
  11. }
  12. var b= a()
  13. b[0]();
  14. b[0]("李四");

複製代碼再寫個

  1. function a(){
  2. var name = null;
  3. var f1 = function(n){
  4. name = n;
  5. };
  6. var f2 = function(){
  7. return name;
  8. };
  9. return {
  10. setName : f1,
  11. getName : f2
  12. }
  13. }
  14. var o =a();
  15. o.setName("老姚");
  16. var myName = o.getName();
  17. console.log(myName);

複製代碼如果在講上述例子改寫新的形式,把函數改成匿名的(有的人甚至覺得匿名函數是閉包,那樣我會說,看來所有函數都是閉包了),就是一種設計模式:模塊模式。

  1. var person = (function(){
  2. var name = null;
  3. var f1 = function(n){
  4. name = n;
  5. };
  6. var f2 = function(){
  7. return name;
  8. };
  9. return {
  10. setName : f1,
  11. getName : f2
  12. }
  13. }
  14. )();
  15. person.setName("老姚");
  16. console.log(person.getName());

複製代碼由此可以看出來應用閉包不只是簡單的寫個計數器啥的。第三部分最後再來看看,如何避免閉包。有時我們本意不想用閉包的,如下,我想彈出0,1,2的,結果都會彈出3.

  1. var fun = function(){
  2. var a = [];
  3. for(var i = 0;i<3;i++){
  4. a.push(function(){
  5. return i;
  6. })
  7. }
  8. //console.log(i);//因為js中沒有塊級作用域,i最後變成3,而不是報錯
  9. return a;
  10. }
  11. var a = fun();
  12. alert(a[0]());//3
  13. alert(a[1]());//3
  14. alert(a[2]());//3

複製代碼可以改成

  1. var fun = function(){
  2. var a = [];
  3. for(var i = 0;i<3;i++){
  4. a.push(function(j){
  5. return function(){
  6. return j;
  7. };
  8. }(i))
  9. }
  10. return a;
  11. }
  12. var a = fun();
  13. alert(a[0]())//0
  14. alert(a[1]())//1
  15. alert(a[2]())//2

複製代碼最開始的那個例子也可以避免閉包,改成

  1. function a(){
  2. var x = 0;
  3. return function(x){
  4. return function(){
  5. x++;//此函數的作用域鏈能看到x
  6. console.log(x);
  7. }
  8. }(x);
  9. }

複製代碼還有一種情況也會出現閉包現象,把內部函數綁定了dom節點某種操作(onclick)的回調函數,沒有寫在return語句裏。道理是一樣的。寫在return裏,是return後調用,綁定到dom上,比如說觸發點擊事件後再調用,其道理是一樣的,作用域鏈條該怎麼找就怎麼找。最後再說一句,閉包最起碼的應用,就是我們可以把一些全局變數封裝起來,通過這種方式來不污染全局,例如上面的模塊模式例子。
推薦閱讀:

查看原文 >>
相關文章