來看一些關於閉包的定義:javascript
this
和arguments
)。 --《JS語言精粹》 p36來個定義總結php
函數
來建立個簡單的閉包前端
var sayName = function(){ var name = 'jozo'; return function(){ alert(name); } }; var say = sayName(); say();
來解讀後面兩個語句:java
var say = sayName()
:返回了一個匿名的內部函數保存在變量say中,而且引用了外部函數的變量name,因爲垃圾回收機制,sayName函數執行完畢後,變量name並無被銷燬。say()
:執行返回的內部函數,依然能訪問變量name,輸出 'jozo' .理解做用域鏈對理解閉包也頗有幫助。算法
變量在做用域中的查找方式應該都很熟悉了,其實這就是順着做用域鏈往上查找的。chrome
當函數被調用時:閉包
做用域鏈:當前函數的活動對象優先級最高,外部函數的活動對象次之,外部函數的外部函數的活動對象依次遞減,直至做用域鏈的末端--全局做用域。優先級就是變量查找的前後順序;模塊化
先來看個普通的做用域鏈:函數
function sayName(name){ return name; } var say = sayName('jozo');
這段代碼包含兩個做用域:a.
全局做用域;b.
sayName函數的做用域,也就是隻有兩個變量對象,當執行到對應的執行環境時,該變量對象會成爲活動對象,並被推入到執行環境做用域鏈的前端,也就是成爲優先級最高的那個。 看圖說話:字體
這圖在JS高級程序設計書上也有,我從新繪了遍。
在建立sayName()函數時,會建立一個預先包含變量對象的做用域鏈,也就是圖中索引爲1的做用域鏈,而且被保存到內部的[[Scope]]屬性中,當調用sayName()函數的時候,會建立一個執行環境,而後經過複製函數的[[Scope]]屬性中的對象構建起做用域鏈,此後,又有一個活動對象(圖中索引爲0)被建立,並被推入執行環境做用域鏈的前端。
通常來講,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。可是,閉包的狀況又有所不一樣 :
再來看看看閉包的做用域鏈:
function sayName(name){ return function(){ return name; } } var say = sayName('jozo');
這個閉包實例比上一個例子多了一個匿名函數的做用域:
在匿名函數從sayName()函數中被返回後,它的做用域鏈被初始化爲包含sayName()函數的活動對象和全局變量對象。這樣,匿名函數就能夠訪問在sayName()中定義的全部變量和參數,更爲重要的是,sayName()函數在執行完畢後,其活動對象也不會被銷燬,由於匿名函數的做用域鏈依然在引用這個活動對象,換句話說,sayName()函數執行完後,其執行環境的做用域鏈會被銷燬,但他的活動對象會留在內存中,知道匿名函數會銷燬。這個也是後面要講到的內存泄露的問題。
做用域鏈問題不寫那麼多了,寫書上的東西也很累 o(╯□╰)o
實例1:實現累加
// 方式1 var a = 0; var add = function(){ a++; console.log(a) } add(); add(); //方式2 :閉包 var add = (function(){ var a = 0; return function(){ a++; console.log(a); } })(); console.log(a); //undefined add(); add(); 相比之下方式2更加優雅,也減小全局變量,將變量私有化
實例2 :給每一個li添加點擊事件
var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = function(){ alert(i); } } console.log(i); // 5 //執行匿名函數 (function(){ alert(i); //5 }());
上面是一個經典的例子,咱們都知道執行結果是都彈出5,也知道能夠用閉包解決這個問題,可是我剛開始始終不能明白爲何每次彈出都是5,爲何閉包能夠解決這問題。後來捋一捋仍是把它弄清晰了:
a. 先來分析沒用閉包前的狀況:for循環中,咱們給每一個li點擊事件綁定了一個匿名函數,匿名函數中返回了變量i的值,當循環結束後,變量i的值變爲5,此時咱們再去點擊每一個li,也就是執行相應的匿名函數(看上面的代碼),這是變量i已是5了,因此每一個點擊彈出5. 由於這裏返回的每一個匿名函數都是引用了同一個變量i,若是咱們新建一個變量保存循環執行時當前的i的值,而後再讓匿名函數應用這個變量,最後再返回這個匿名函數,這樣就能夠達到咱們的目的了,這就是運用閉包來實現的!
b. 再來分析下運用閉包時的狀況:
var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = (function(num){ var a = num; // 爲了說明問題 return function(){ alert(a); } })(i) } console.log(i); // 5
這裏for循環執行時,給點擊事件綁定的匿名函數傳遞i後當即執行返回一個內部的匿名函數,由於參數是按值傳遞的,因此此時形參num保存的就是當前i的值,而後賦值給局部變量 a,而後這個內部的匿名函數一直保存着a的引用,也就是一直保存着當前i的值。 因此循環執行完畢後點擊每一個li,返回的匿名函數執行彈出各自保存的 a 的引用的值。
咱們來看看閉包的用途。事實上,經過使用閉包,咱們能夠作不少事情。好比模擬面向對象的代碼風格;更優雅,更簡潔的表達出代碼;在某些方面提高代碼的執行效率。
1. 匿名自執行函數
咱們在實際狀況下常常遇到這樣一種狀況,即有的函數只須要執行一次,其內部變量無需維護,好比UI的初始化,那麼咱們可使用閉包:
//將所有li字體變爲紅色 (function(){ var els = document.getElementsByTagName('li'); for(var i = 0,lng = els.length;i < lng;i++){ els[i].style.color = 'red'; } })();
咱們建立了一個匿名的函數,並當即執行它,因爲外部沒法引用它內部的變量,
所以els,i,lng這些局部變量在執行完後很快就會被釋放,節省內存!
關鍵是這種機制不會污染全局對象。
2. 實現封裝/模塊化代碼
var person= function(){ //變量做用域爲函數內部,外部沒法訪問 var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }(); console.log(person.name);//直接訪問,結果爲undefined console.log(person.getName()); //default person.setName("jozo"); console.log(person.getName()); //jozo
3. 實現面向對象中的對象
這樣不一樣的對象(類的實例)擁有獨立的成員及狀態,互不干涉。雖然JavaScript中沒有類這樣的機制,可是經過使用閉包,
咱們能夠模擬出這樣的機制。仍是以上邊的例子來說:
function Person(){ var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }; var person1= Person(); print(person1.getName()); john.setName("person1"); print(person1.getName()); // person1 var person2= Person(); print(person2.getName()); jack.setName("erson2"); print(erson2.getName()); //person2
Person的兩個實例person1 和 person2 互不干擾!由於這兩個實例對name這個成員的訪問是獨立的 。
垃圾回收機制
說到內存管理,天然離不開JS中的垃圾回收機制,有兩種策略來實現垃圾回收:標記清除 和 引用計數;
標記清除:
垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記,而後,它會去掉環境中的變量的標記和被環境中的變量引用的變量的標記,此後,若是變量再被標記則表示此變量準備被刪除。 2008年爲止,IE,Firefox,opera,chrome,Safari的javascript都用使用了該方式;
引用計數:
跟蹤記錄每一個值被引用的次數,當聲明一個變量並將一個引用類型的值賦給該變量時,這個值的引用次數就是1,若是這個值再被賦值給另外一個變量,則引用次數加1。相反,若是一個變量脫離了該值的引用,則該值引用次數減1,當次數爲0時,就會等待垃圾收集器的回收。
這個方式存在一個比較大的問題就是循環引用,就是說A對象包含一個指向B的指針,對象B也包含一個指向A的引用。 這就可能形成大量內存得不到回收(內存泄露),由於它們的引用次數永遠不多是 0 。早期的IE版本里(ie4-ie6)採用是計數的垃圾回收機制,閉包致使內存泄露的一個緣由就是這個算法的一個缺陷。
咱們知道,IE中有一部分對象並非原生額javascript對象,例如,BOM和DOM中的對象就是以COM對象的形式實現的,而COM對象的垃圾回收機制採用的就是引用計數。所以,雖然IE的javascript引擎採用的是標記清除策略,可是訪問COM對象依然是基於引用計數的,所以只要在IE中設計COM對象就會存在循環引用的問題!
舉個栗子:
window.onload = function(){ var el = document.getElementById("id"); el.onclick = function(){ alert(el.id); } }
這段代碼爲何會形成內存泄露?
el.onclick= function () { alert(el.id); };
執行這段代碼的時候,將匿名函數對象賦值給el的onclick屬性;而後匿名函數內部又引用了el對象,存在循環引用,因此不能被回收;
解決方法:
window.onload = function(){ var el = document.getElementById("id"); var id = el.id; //解除循環引用 el.onclick = function(){ alert(id); } el = null; // 將閉包引用的外部函數中活動對象清除 }
優勢:
缺點