淺談js之閉包


1.什么是閉包???

"官方"的解釋是指一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分;

紅皮書是這樣說的,閉包是指有權訪問另一個函數作用域中變量的函數;常見的創建閉包的方式就是在一個函數中再創建一個函數;

閉包是一種特殊的對象。它由兩部分構成:函數,以及創建該函數的環境。環境由閉包創建時在作用域中的任何局部變量組成;

光看定義是雲里霧里,但是到了真正的代碼了又是什么樣的形式呢?經典的閉包例子:

function fn() {
    var name = 4;
    return function () {
        var n = 0;
        alert(++n);
        alert(++name);
    }
}
var fun = fn();
fun();//n =>1,name=>5;
fun();// n =>1,name=>6;

 

這里就有閉包產生了,fun就是閉包;這個閉包由fn中的匿名函數和name變量構成。,但是呢,它又是一種特殊的函數,它是一種能能夠讀取其他函數內部變量的特殊函數;fn中的匿名函數的父函數在執行完了之后,按正常來說,它應該被銷毀,里面的變量也被銷毀,,但是里面的變量現在沒有被銷毀,而是被這個匿名函數引用着;(說實在的它不應該被銷毀,因為這個匿名函數還沒有執行,還要用上一級的函數的中的變量,你給我銷毀了,我可怎么辦,那不是讓我報錯呀,但是呢,我給你用,不銷毀,這又不符合規矩,按規矩是這樣的:當一個函數執行完之后,是要被立即銷毀的,執行環境銷毀,里面的變量銷毀,你現在不讓我銷毀,那不亂套了,那怎么辦呢,於是乎,一群磚家,就說趕這種方式叫閉包吧)意思是說我跟你們不一樣;因為是特殊函數,代碼的最后一句fun()執行完之后,name變量還是沒有釋放,但是每次執行fun,里面的n變量是都是新創建的,執行完之后又釋放掉;要是你明白了就不需要看括號里的內容了(這里我就形象的說為什么說name變量會一直在內存中?在剛開始的時候,父函數fn在剛要執行完了,開始銷毀時,匿名子函數就說了,我要用你的name變量,你先別銷毀了,父函數說好吧,於是乎,父函數執行完之后(歸天了),就沒有銷毀name,在當你調用fun,執行匿名子函數,fun()調用完了,你把自己家的n變量銷毀了,fun就說了name又不是我的東西,我就是用了一下,憑什么我給銷毀,我不給銷毀,但是這時父函數已經去世了(執行完了),於是就產生了內存消耗,除非你手動銷毀,垃圾收回機制不會自動收回;這又牽扯到內存泄漏,性能問題了,后面說。)

作用域鏈:

講到這里,如果要想整整的明白還有知道作用域鏈,和垃圾收回機制;

我就說說上面代碼執行時的作用域鏈:

我就說說這張圖是什么意思,這種圖是執行var fun = fu();fun();這兩句代碼時所發生的情況;

其實匿名函數在fu()被返回時,它的作用域鏈就被初始化為包含全局變量對象和fu函數的活動對象;也就是說當fu函數執行完返回后,它的執行環境會被銷毀,但是其活動對象不會被銷毀,仍然在內存中,因為匿名函數的作用域鏈中引用了這個活動對象。只有到匿名函數被手動銷毀時才銷毀;其實在fu執行完后,紅字顯示的部分就消失了,就活動變量沒有消失;

再說一點關於作用域鏈的問題:

1。作用域鏈中的變量對象(函數中叫活動對象)保存的是變量和函數;

2.作用域鏈的作用就是為了保證對執行環境有權訪問的所有變量和函數的有序訪問。

3.查找一個變量是從作用域鏈的最前端,逐漸向上找,只要找到就不再向上找了,不論上面是否還有這個值;

4.就以上面的fu函數為例吧,在聲明fu函數的時候就開始預先創建一個包含全局變量對象的作用域鏈了(如果嵌套多了,其實就是在函數聲明的地方創建父函數及其之上的作用域鏈),這個作用域鏈將被保存在剛創建函數的內部[[Scope]]的屬性中;當調用fu函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]中的作用域鏈構建起執行環境的作用域鏈;之后還要創建一個本函數的活動對象,並把這個活動對象推入執行環境作用域鏈的前端。

5.作用域鏈本質上就是一個指向變量對象的指針列表。

垃圾回收機制:

再說說垃圾回收機制:

1.如果一個對象不再有引用了,這個對象就會被GC收回;

2,如果兩個對象互相引用,但不被第三個引用,這兩個互相引用的對象也會收回的。

而閉包不再這個范疇之內。

閉包的特性:

1.引用的變量不被垃圾回收機制收回

2.函數內部可以引用外部的變量;

3.函數里面嵌套函數

閉包的用處(好處):

1.私有變量和方法

var a=(function() {
    var privateNum = 1;
    function privateFun(val) {
        alert(val);
    }
    return {
        publicFun: function() {
            privateFun(2);
        },
        publicNum:function() {
            return privateNum;
        }
    }

    })();
    a.publicNum();//1
    a.publicFun();//2

如果你用a.privateNum,a.privateFun();這是會報錯的。

2.實現一些變量的累加

 

function a() {
    var n=0;
    return function () {
        n++;
        alert(n);
    }
}
var b = a();
b();//1
b();//2
b();//3

這里只是要使用累加,就這樣干,具體還要具體分析,原理是這樣了

因閉包產生的問題

初學者常見的,循環閉包

大部分我們所寫的 Web JavaScript 代碼都是事件驅動的 — 定義某種行為,然后將其添加到用戶觸發的事件之上(比如點擊或者按鍵)。我們的代碼通常添加為回調:響應事件而執行的函數。

<!DOCTYPE HTML>
 <html>
  <head>
   <meta charset="utf-8"/>
   <title>閉包循環問題</title>
   <style type="text/css">
     p {background:red;}
   </style>
 </head> 
 <body> 
  <p class="p">我是1號</p>
  <p class="p">我是2號</p>
  <p class="p">我是3號</p>
  <p class="p">我是4號</p>
  <p class="p">我是五號</p>
<script type="text/javascript">
var page = document.getElementsByTagName("p");
for(var i=0; i<page.length; i++) {
  page[i].onclick = function () {
    alert("我是"+i+"號");
  }
} 
</script> 
</body> 
</html>
閉包循環問題

你不管點擊哪一個,都alert”我是5號“;
原因就是你循環了五次產生了五個閉包,而這5個閉包共享一個變量i,說的明白一點就是,在for循環結束時,只是把這五個匿名函數注冊給click事件,當時在循環的時候並沒有執行,當循環結束了,此時i的值是5;之后你去點擊p標簽,你點擊哪一個就執行哪一個對應的匿名函數(這個時候才執行),這時候匿名中發現一個i,匿名中沒有定義i,於是沿着作用域鏈找,找到了,但是這時候循環早就結束了,i等於5,於是彈出”我是5號“來;點擊其他的同理;

怎么解決呢:

一種方法是再創建一個閉包,把js代碼改為這樣就行了

var page = document.getElementsByTagName("p");
for(var i=0; i<page.length; i++) {
  !function(num) {
  page[i].onclick = function () {
    alert("我是"+num+"號");
  }
  }(i)
}

我只說一點,這次五個閉包不共享num,而是創建五個num變量

還有一種解決方式:

var page = document.getElementsByTagName("p");
for(var i=0; i<page.length; i++) {
  page[i].num = i//先把每個變量值存起來
  page[i].onclick = function () {
    alert("我是"+this.num+"號");
  }
} 

閉包中的this對象

var num = 1;
var obj = {
  num:2,
  getNum:function() {
    return function () {
      return this.num;
    }
}
}
alert(obj.getNum()());//num -> 1

為什么不彈出2呢,這里是說明閉包中你需要注意現在的this的指向那一個對象,其實記住一句話就永遠不會用錯this的指向問題,this永遠指向調用它的作用域;

如果這樣寫你就可能理解了

var num = 1;
var obj = {
  num:2,
  getNum:function() {
    return function () {
      return this.num;
    }
}
}
var a = obj.getNum();
alert(window.a());//1

其實是window對象調用的,這就是說閉包中的this讓你看不清this的指向;

要是讓它alert 2你要這樣:

var num = 1;
var obj = {
  num:2,
  getNum:function() {
    var _this = this;//在這里保存this
    return function () {
      return _this.num;
    }
}
}
var a = obj.getNum();
alert(window.a());

性能考量

如果不是因為某些特殊任務而需要閉包,在沒有必要的情況下,在其它函數中創建函數是不明智的,因為閉包對腳本性能具有負面影響,包括處理速度和內存消耗。

例如,在創建新的對象或者類時,方法通常應該關聯於對象的原型,而不是定義到對象的構造器中。原因是這將導致每次構造器被調用,方法都會被重新賦值一次(也就是說,為每一個對象的創建)。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

應該是這樣

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

示例中,繼承的原型可以為所有對象共享,且不必在每一次創建對象時定義方法

 歡迎評論指正;

參考:

紅皮書

Mozilla:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures

阮一峰:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM