[ JS 進階 ] 閉包,作用域鏈,垃圾回收,內存泄露


kraaas 2015年05月19日發佈
  • 14.3k 次瀏覽
關於閉包,我翻了幾遍書,看了幾遍視頻,查了一些資料,可是還是迷迷糊糊的,乾脆自己動手來個總結吧 !歡迎指正... (~ o ~)~zZ
1. 什麼是閉包?
來看一些關於閉包的定義:
  1. 閉包是指有權訪問另一個函數作用域中變量的函數 --《JS高級程序設計第三版》 p178
  2. 函數對象可以通過作用域鏈相關聯起來,函數體內部的變量都可以保存在函數作用域內,這種特性稱爲 ‘閉包’ 。 --《JS權威指南》 p183
  3. 內部函數可以訪問定義它們的外部函數的參數和變量(除了thisarguments)。 --《JS語言精粹》 p36
來個定義總結
  1. 可以訪問外部函數作用域中變量的函數
  2. 被內部函數訪問的外部函數的變量可以保存在外部函數作用域內而不被回收---這是核心,後面我們遇到閉包都要想到,我們要重點關注被閉包引用的這個變量。
來創建個簡單的閉包

var sayName = function(){
var name = 'jozo';
return function(){
alert(name);
}};
var say = sayName();
say();

來解讀後面兩個語句:
  • var say = sayName() :返回了一個匿名的內部函數保存在變量say中,並且引用了外部函數的變量name,由於垃圾回收機制,sayName函數執行完畢後,變量name並沒有被銷燬。
  • say() :執行返回的內部函數,依然能訪問變量name,輸出 'jozo' .
2. 閉包中的作用域鏈
理解作用域鏈對理解閉包也很有幫助。
變量在作用域中的查找方式應該都很熟悉了,其實這就是順着作用域鏈往上查找的。
當函數被調用時:
  1. 先創建一個執行環境(execution context),及相應的作用域鏈;
  2. 將arguments和其他命名參數的值添加到函數的活動對象(activation object)
作用域鏈:當前函數的活動對象優先級最高,外部函數的活動對象次之,外部函數的外部函數的活動對象依次遞減,直至作用域鏈的末端--全局作用域。優先級就是變量查找的先後順序;
先來看個普通的作用域鏈:

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
3. 閉包的實例
實例1:實現累加
// 方式1
var a = 0;
var add = function(){
a++;
console.log(a)
}
add();

//方式2 :閉包
var add = (function(){
var a = 0;
return function(){
a++;
console.log(a);
}
})();
console.log(a); //undefined
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,爲什麼閉包可以解決這問題。後來捋一捋還是把它弄清晰了:
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 的引用的值。
4. 閉包的運用
我們來看看閉包的用途。事實上,通過使用閉包,我們可以做很多事情。比如模擬面向對象的代碼風格;更優雅,更簡潔的表達出代碼;在某些方面提升代碼的執行效率。
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這個成員的訪問是獨立的 。
5. 內存泄露及解決方案
垃圾回收機制
說到內存管理,自然離不開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; // 將閉包引用的外部函數中活動對象清除}
6. 總結閉包的優缺點
優點:
  • 可以讓一個變量常駐內存 (如果用的多了就成了缺點
  • 避免全局變量的污染
  • 私有化變量
缺點
  • 因爲閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的內存
  • 引起內存泄露
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章