JavaScript 的閉包原理與詳解

JavaScript 的閉包原理與詳解。

JavaScript的閉包是一個特色,但也是很多新手難以理解的地方,閱讀過不少大作,對閉包講解不一,個人以爲,在《JavaScript高級程序設計》一書中,解釋的最爲詳盡,結合此書,表述一下我對JavaScript閉包的理解,希望能對新手有些幫助。

閉包的例子

var count=10;//全局作用域 標記爲flag1
function add(){
    var count=0;//函數全局作用域 標記爲flag2
    return function(){
        count+=1;//函數的內部作用域
        alert(count);
    }
}
var s=add()
s();//輸出1
s();//輸出2

來看一下發生了什麼吧,add()的返回值是一個函數,首先第一次調用s()的時候,是執行add()的返回的函數,也就是下面這個函數:

function(){
        count+=1;//函數的內部作用域
        alert(count);
    }

也就是將count+1,在輸出,那count是從哪兒來的的呢,根據作用域鏈的規則,底層作用域沒有聲明的變量,會向上一級找,找到就返回,沒找到就一直找,直到window的變量,沒有就返回undefined。這裏明顯count 是函數內部的flag2 的那個count ,

var count=10;//全局作用域
function add(){
    //var count=0;註釋掉了
    return function(){
        count+=1;//函數的內部作用域
        alert(count);
    }
}
var s=add()
s();//輸出11
s();//輸出12
自然這是體現不出閉包的性質,只爲了說明函數作用域鏈
繼續說明:第一次執行,是沒有疑問的輸出1,那第二次的過程是怎樣的呢?
繼續執行那個函數的返回的方法,還是count+=1;然後再輸出count ,這裏問題就來了,不應該繼續向上尋找,找到count=0;然後輸出1嗎?不知道有沒有注意一個問題,那就是s()執行的是下面這個函數




function(){
        count+=1;//函數的內部作用域
        alert(count);
    }
而不是
function add(){
    var count=0;//函數全局作用域 標記爲flag2
    return function(){
        count+=1;//函數的內部作用域
        alert(count);
    }
}

也就是說add(),只被執行了一次。然後執行兩次s(),那count的值就是隻聲明瞭一次。

var s=add(),函數add 只在這裏執行了一次。

下面執行的都是s(),那第二次的count的值是從哪兒來的,沒錯它還是第一次執行add時,留下來的那個變量。

(這怎麼可能,函數變量執行完就會被釋放啊,爲什麼還在?這裏就是一個垃圾回收機制的引用計數問題)。

“”如果一個變量的引用不爲0,那麼他不會被垃圾回收機制回收,引用,就是被調用“”。

由於再次執行s()的時候,再次引用了第一次add()產生的變量count ,所以count沒有被釋放,第一次s(),count 的值爲1,第二次執行s(),count的值再加1,自然就是2了。

讓我們返回來再看看,根據以上所說,如果執行兩次add() ,那就應該輸出 都是1,來改一下這個函數

function add(){
    var count=0;//函數全局作用域
    return function(){
        count+=1;//函數的內部作用域
        alert(count);
    }
}
add()();//輸出1
add()();//輸出1
O(∩_∩)O哈哈~果真如此。輸出的兩次都是1. 不知道通過這個示例,你有沒有理解了閉包。 描述一下閉包的結構吧,爲什麼閉包一般都需要一個匿名函數,爲了實現作用域鏈的規則,需要有兩層作用域。 想來大家都應該理解了。 下面再描述一個常見的錯誤
<p>1</p><p>2</p><p>3</p>
<p>4</p><p>5</p><p>6</p>

var plist=document.getElementsByTagName('p');
for (var i=0;i<plist.length;i++) {
    plist[i].onclick=function(){
        alert(plist[i].innerHTML)//全是undefined
    }
}
想要點擊相應的p 彈出對應的i的值,但是這裏發生了什麼,點擊任意一個數,彈出的都是undefined,我的天,不應該是1,2,3,4,5,6的嗎? 解釋一下,函數執行完,i 的值是6 ,沒有錯吧。那plist[6]是不是undefined。 我們點擊的時候,觸發的就是輸出undefined,(づ。◕‿‿◕。)づ,但我們想要的值是點擊對應的p 的innerHTML, alert(this.innerHTML)//萬事大吉, this 是一個好東西,指向當前對象當我們綁定的時候綁定的就是當前值,而不是動態的i的值,前面綁定的是一個動態i 的值,這裏也可以使用閉包解決,不過不推薦,畢竟閉包使用的話,會讓內存無法釋放,也就是閉包越多,佔的內存越多。使用需謹慎。

總結一下

JavaScript閉包的形成原理是基於函數變量作用域鏈的規則 和 垃圾回收機制的引用計數規則。
JavaScript閉包的本質是內存泄漏,指定內存不釋放。
(不過根據內存泄漏的定義是無法使用,無法回收來說,這不是內存泄漏,由於只是無法回收,但是可以使用,爲了使用,不讓系統回收)
JavaScript閉包的用處,私有變量,獲取對應值等,。。


下面這些是引用自原書的原文,有興趣的朋友可以讀一下,加深理解。有關於作用域鏈和引用計數的問題也有說明。

以下內容引用自《JavaScript高級程序設計》


執行環境及作用域

念。執行環境定義了變量或函數有權訪問的其他數據,決定了它們各自的行爲。每個執行環境都有一個與之關聯的變量對象(variable object),環境中定義的所有變量和函數都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數據時會在後臺使用它。
全局執行環境是最外圍的一個執行環境。根據 ECMAScript 實現所在的宿主環境不同,表示執行環境的對象也不一樣。在 Web 瀏覽器中,全局執行環境被認爲是 window 對象(第 7 章將詳細討論),因此所有全局變量和函數都是作爲 window 對象的屬性和方法創建的。某個執行環境中的所有代碼執行完畢後,該環境被銷燬,保存在其中的所有變量和函數定義也隨之銷燬(全局執行環境直到應用程序退出——例如關閉網頁或瀏覽器——時纔會被銷燬)。
每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之後,棧將其環境彈出,把控制權返回給之前的執行環境ECMAScript 程序中的執行流正是由這個方便的機制控制着。
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象(activation object)作爲變量對象。活動對象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是作用域鏈中的最後一個對象。
標識符解析是沿着作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,然後逐級地向後回溯,直至找到標識符爲止(如果找不到標識符,通常會導致錯誤發生)。請看下面的示例代碼:
var color = "blue";
function changeColor(){
 if (color === "blue"){
 color = "red";
 } else {
 color = "blue";
 }
}
changeColor();
alert("Color is now " + color);
在這個簡單的例子中,函數 changeColor()的作用域鏈包含兩個對象:它自己的變量對象(其中定義着 arguments 對象)和全局環境的變量對象。可以在函數內部訪問變量 color,就是因爲可以在這個作用域鏈中找到它。
此外,在局部作用域中定義的變量可以在局部環境中與全局變量互換使用,如下面這個例子所示: 
var color = "blue";
function changeColor(){
 var anotherColor = "red";
 function swapColors(){
 var tempColor = anotherColor;
 anotherColor = color;
 color = tempColor;

 // 這裏可以訪問 color、anotherColor 和 tempColor
 }
 // 這裏可以訪問 color 和 anotherColor,但不能訪問 tempColor
 swapColors();
}
// 這裏只能訪問 color
changeColor();
以上代碼共涉及 3 個執行環境:
全局環境、changeColor()的局部環境和 swapColors()的局部環境。全局環境中有一個變量 color 和一個函數 changeColor()。changeColor()的局部環境中有一個名爲 anotherColor 的變量和一個名爲 swapColors()的函數,但它也可以訪問全局環境中的變量 color。swapColors()的局部環境中有一個變量 tempColor,該變量只能在這個環境中訪問到。
無論全局環境還是 changeColor()的局部環境都無權訪問 tempColor。然而,在 swapColors()內部則可以訪問其他兩個環境中的所有變量,因爲那兩個環境是它的父執行環境。圖 4-3 形象地展示了前面這個例子的作用域鏈。
圖 4-3

這裏寫圖片描述

圖 4-3 中的矩形表示特定的執行環境。其中,內部環境可以通過作用域鏈訪問所有的外部環境,但外部環境不能訪問內部環境中的任何變量和函數。這些環境之間的聯繫是線性、有次序的。每個環境都可以向上搜索作用域鏈,以查詢變量和函數名;但任何環境都不能通過向下搜索作用域鏈而進入另一個執行環境。對於這個例子中的 swapColors()而言,其作用域鏈中包含 3 個對象:swapColors()的變量對象、changeColor()的變量對象和全局變量對象。swapColors()的局部環境開始時會先在自己的變量對象中搜索變量和函數名,如果搜索不到則再搜索上一級作用域鏈。changeColor()的作用域鏈中只包含兩個對象:它自己的變量對象和全局變量對象。這也就是說,它不能訪問 swapColors()的
環境。
———-

JavaScript的垃圾回收機制和引用計數規則

垃圾收集
JavaScript 具有自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。而在 C 和 C++之類的語言中,開發人員的一項基本任務就是手工跟蹤內存的使用情況,這是造成許多問題的一個根源。在編寫 JavaScript 程序時,開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收完全實現了自動管理。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變量,然後釋放其佔用的內存。爲此,垃圾收集器會按照固定的時間間隔(或代碼執行中預定的收集時間),週期性地執行這一操作。下面我們來分析一下函數中局部變量的正常生命週期。局部變量只在函數執行的過程中存在。而在這個過程中,會爲局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。然後在函數中使用這些變量,直至函數執行結束。此時,局部變量就沒有存在的必要了,因此可以釋放它們的內存以供將來使用。在這種情況下,很容易判斷變量是否還有存在的必要;但並非所有情況下都這麼容易就能得出結論。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用,對於不再有用的變量打上標記,以備將來收回其佔用的內存。用於標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則通常有兩
個策略。

標記清除

JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記爲“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,因爲只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記爲“離開環境”。可以使用任何方式來標記變量。比如,可以通過翻轉某個特殊的位來記錄一個變量何時進入環境,或者使用一個“進入環境的”變量列表及一個“離開環境的”變量列表來跟蹤哪個變量發生了變化。說到底,如何標記變量其實並不重要,關鍵在於採取什麼策略。垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。然後,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此之後再被加上標記的變量將被視爲準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最後,垃圾收集器完成內存清除工作,銷燬那些帶標記的值並回收它們所佔用的內存空間。

引用計數

 
另一種不太常見的垃圾收集策略叫做引用計數(reference counting)。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是 1。如果同一個值又被賦給另一個變量,則該值的引用次數加 1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數爲零的值所佔用的內存。Netscape Navigator 3.0 是最早使用引用計數策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循環引用。循環引用指的是對象 A 中包含一個指向對象 B 的指針,而對象 B 中也包含一個指向對象 A 的引用。請看下面這個例子:


function problem(){
 var objectA = new Object();
 var objectB = new Object();
 objectA.someOtherObject = objectB;
 objectB.anotherObject = objectA;
}
在這個例子中,objectA 和 objectB 通過各自的屬性相互引用;也就是說,這兩個對象的引用次數都是 2。在採用標記清除策略的實現中,由於函數執行之後,這兩個對象都離開了作用域,因此這種相互引用不是個問題。但在採用引用計數策略的實現中,當函數執行完畢後,objectA 和 objectB 還將繼續存在,因爲它們的引用次數永遠不會是 0。假如這個函數被重複多次調用,就會導致大量內存得不到回收。爲此,Netscape 在 Navigator 4.0 中放棄了引用計數方式,轉而採用標記清除來實現其垃圾收集機制。可是,引用計數導致的麻煩並未就此終結。我們知道,IE 中有一部分對象並不是原生 JavaScript 對象。例如,其 BOM 和 DOM 中的對象就是使用 C++以 COM(Component Object Model,組件對象模型)對象的形式實現的,而 COM 對象的垃圾收集機制採用的就是引用計數策略。因此,即使 IE 的 JavaScript 引擎是使用標記清除策略來實現的,但JavaScript 訪問的 COM 對象依然是基於引用計數策略的。換句話說,只要在 IE 中涉及 COM 對象,就會存在循環引用的問題。下面這個簡單的例子,展示了使用 COM 對象導致的循環引用問題:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
這個例子在一個 DOM 元素(element)與一個原生 JavaScript 對象(myObject)之間創建了循環引用。其中,變量 myObject 有一個名爲 element 的屬性指向 element 對象;而變量 element 也有一個屬性名叫 someObject 回指 myObject。由於存在這個循環引用,即使將例子中的 DOM 從頁面中移除,它也永遠不會被回收。爲了避免類似這樣的循環引用問題,最好是在不使用它們的時候手工斷開原生 JavaScript 對象與DOM 元素之間的連接。例如,可以使用下面的代碼消除前面例子創建的循環引用:
myObject.element = null;
element.someObject = null;
將變量設置爲 null 意味着切斷變量與它此前引用的值之間的連接。當垃圾收集器下次運行時,就會刪除這些值並回收它們佔用的內存。爲了解決上述問題,IE9 把 BOM 和 DOM 對象都轉換成了真正的 JavaScript 對象。這樣,就避免了兩種垃圾收集算法並存導致的問題,也消除了常見的內存泄漏現象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章