javascript
javascript之闭包深入理解(一)
曾經在開始學習javascript的時候,很是不理解閉包的概念。今天想對它詳細的剖析。
在說清楚閉包之前,必須先清楚作用域鏈。
?
我們知道,執行環境是js中最為重要的一個概念。執行環境定義了變量或函數有權訪問的的其它數據,決定了它們各自的行為。每個執行環境都有一個與之關聯的變量對象,執行環境定義的所有變量和函數都保存在這個對象中,雖然我們編寫的代碼無法訪問這個對象,但是解釋器在處理數據時會在后臺使用它。
全局執行環境是最外圍的一個執行環境。所在的宿主環境不同,表示執行環境的對象也不一樣。在Web瀏覽器中,全局執行環境被認為是window對象,因此所有的全局變量和函數都是作為window對象的屬性和方法創建的。某個執行環境中的所有代碼被執行完畢后,該環境就會被銷毀,保存在其中的所有變量和函數定義也會被銷毀。全局執行環境直到應用程序退出,web瀏覽器的全局執行環境被銷毀的時機是瀏覽器的關閉時刻。
每一個函數都有自己的執行環境。當執行流進入到一個函數時,函數的環境就會被推入一個環境棧中。當函數執行完畢后,棧將其環境彈出,把控制權返回給前一個執行環境。ECMAScript程序中的執行流正式由這個方便的機制控制著。
當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。作用域鏈的用途是保證對執行環境有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象。如果這個環境是函數,則將其活動對象作為變量對象。活動對象在最開始的時候只包含一個變量,即arguments對象。作用域鏈中的下一個變量對象來自包含外部環境,而下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境。請看如下代碼,
// window var x=1; function fn1(args1){// do sth1var y=1;function fn2(args2){// do sth2for(var i=1;i<10;i++){// do sth3 }} }那么它的作用域鏈參照下圖:
?標識符的解析是沿著作用域鏈一級一級的向上搜索標識符的過程,搜索過程始終是從作用域鏈的前端開始,然后逐級的向后回溯,直到找到標識符為止,如果找不到標識符,通常會發生錯誤 ,也就是我們經常看到的undefined之類的錯誤。任何環境都不可以向下搜索作用域鏈而進入下一級的搜索環境。既然作用域鏈只可以向上進行搜索,那么有沒有辦法來延長作用域鏈呢?
?
2.延長作用域鏈
?有些語句可以在作用域的前端臨時增加一個變量對象,該變量會在代碼被執行后移除。在兩種情況下會發生這種現象。
- with語句
- try{}catch{}語句
這兩個語句都會在作用域鏈的前端添加一個變量對象。對with 語句來說,會將指定的對象添加到作用域鏈中。對catch 語句來說,會創建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。
3.垃圾回收機制
Javascript具有自動垃圾回收機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。在編寫javascript時,開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收完全是自動管理的。這種垃圾回收機制其實很簡單:找出那些不再繼續使用的變量,然后釋放其占用的內存。為此,垃圾回收器會按照固定的時間周期或在代碼執行中預訂的時間來執行垃圾回收。
局部變量只在函數的執行過程中存在,而在這個過程中,會為局部變量在棧或堆上內存上分配相應的空間,以便存儲它們的值。在函數中會使用這些值,但是在函數執行完畢后,這些值就沒有必要存在了,因此可以釋放它們的內存。在這種情況下,很容易判斷變量是否還有存在的必要,但是并非所有情況都這么容易得出結論。垃圾回收器會跟蹤哪個變量有用,哪個變量沒有用,對于不再用的變量打上標記,以備將來回收并占用其內存。用于標識無用變量的策略可能會因為實現而有所異,其中就有閉包這種特殊情況。
標記清除
javascript最常用的標記清除機制。當變量進入環境中,就將這個變量進行標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境中的變量所占用的內存。當變量離開環境時,就將其標記為“離開環境”。
可以使用任何方式來對變量進行標記,例如可以通過翻轉某個特殊的位來記錄變量是否進入了環境,或者變量列表來進行標記。
垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記。到2008 年為止,IE、Firefox、Opera、Chrome 和Safari 的JavaScript 實現使用的都是標記清除式的
垃圾收集策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。
引用計數
另一種不太常見的垃圾收集策略叫做引用計數(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; }
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 對象。這樣,就避免了兩種垃圾收集算法并存導致的問題,也消除了常見的內存泄漏現象。
3.性能問題
? 垃圾收集器是周期性運行的,而且如果為變量分配的內存數量很可觀,那么回收工作量也是相當大的。在這種情況下,確定垃圾收集的時間間隔是一個非常重要的問題。說到垃圾收集器多長時間運行一次,不禁讓人聯想到IE 因此而聲名狼藉的性能問題。IE 的垃圾收集器是根據內存分配量運行的,具體一點說就是256 個變量、4096 個對象(或數組)字面量和數組元素(slot)或者64KB 的字符串。達到上述任何一個臨界值,垃圾收集器就會運行。這種實現方式的問題在于,如果一個腳本中包含那么多變量,那么該腳本很可能會在其生命周期中一直保有那么多的變量。而這樣一來,垃圾收集器就不得不頻繁地運行。結果,由此引發的嚴重性能問題促使IE7 重寫了其垃圾收集例程。
隨著IE7 的發布,其JavaScript 引擎的垃圾收集例程改變了工作方式:觸發垃圾收集的變量分配、字面量和(或)數組元素的臨界值被調整為動態修正。IE7 中的各項臨界值在初始時與IE6 相等。如果垃圾收集例程回收的內存分配量低于15%,則變量、字面量和(或)數組元素的臨界值就會加倍。如果例程回收了85%的內存分配量,則將各種臨界值重置回默認值。這一看似簡單的調整,極大地提升了IE在運行包含大量JavaScript 的頁面時的性能。
事實上,在有的瀏覽器中可以觸發垃圾收集過程,但不建議這樣做。在IE 中,調用window.CollectGarbage()方法會立即執行垃圾收集。在Opera 7 及更高版本中,調用window.opera.collect()也會啟動垃圾收集例程。
4.內存管理
使用具備垃圾收集機制的語言編寫程序,開發人員一般不必操心內存管理的問題。但是,JavaScript在進行內存管理及垃圾收集時面臨的問題還是有點與眾不同。其中最主要的一個問題,就是分配給Web瀏覽器的可用內存數量通常要比分配給桌面應用程序的少。這樣做的目的主要是出于安全方面的考慮,目的是防止運行JavaScript 的網頁耗盡全部系統內存而導致系統崩潰。內存限制問題不僅會影響給變量分配內存,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。因此,確保占用最少的內存可以讓頁面獲得更好的性能。而優化內存占用的最佳方式,就是為執行中的代碼只保存必要的數據。一旦數據不再有用,最好通過將其值設置為null 來釋放其引用——這個做法叫做解除引用(dereferencing)。這一做法適用于大多數全局變量和全局對象的屬性。局部變量會在它們離開執行環境時自動被解除引用,如下面這個例子所示:
function createPerson(name){var localPerson = new Object();localPerson.name = name;return localPerson; } var globalPerson = createPerson("Nicholas"); // 手工解除globalPerson 的引用 globalPerson = null;在這個例子中,變量globalPerson 取得了createPerson()函數返回的值。在createPerson()函數內部,我們創建了一個對象并將其賦給局部變量localPerson,然后又為該對象添加了一個名為name 的屬性。最后,當調用這個函數時,localPerson 以函數值的形式返回并賦給全局變量globalPerson。由于localPerson 在createPerson()函數執行完畢后就離開了其執行環境,因此無需我們顯式地去為它解除引用。但是對于全局變量globalPerson 而言,則需要我們在不使用它的時候手工為它解除引用,這也正是上面例子中最后一行代碼的目的。不過,解除一個值的引用并不意味著自動回收該值所占用的內存。解除引用的真正作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。
?
轉載于:https://www.cnblogs.com/dacuotecuo/p/3470702.html
總結
以上是生活随笔為你收集整理的javascript之闭包深入理解(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基的变换
- 下一篇: gradle idea java ssm