js閉包測試

本文的誕生,源自近期打算作的一個關於javascript中的閉包的專題,因爲須要解析閉包對垃圾回收的影響,特此針對不一樣的javascript引擎,作了相關的測試。javascript

爲了能從本文中獲得須要的知識,看本文前,請明確本身知道閉包的概念,並對垃圾回收的經常使用算法有必定的瞭解。java

問題的提出
假設有以下的代碼:算法

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function() {
        console.log('inner');
    };
}
var inner = outer();

在這一段代碼中,outer函數和inner函數間會造成一個閉包,導致inner函數可以訪問到largeObject,可是顯然inner並無訪問largeObject,那麼在閉包中的largeObject對象是否能被回收呢?瀏覽器

若是引入更復雜的狀況:閉包

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var anotherLargeObject = LargeObject.fromSize('100MB');
 
    return function() {
        largeObject.work();
        console.log('inner');
    };
}
var inner = outer();

首先一個顯然的概念是largeObject確定不能被回收,由於inner確實地須要使用它。可是anotherLargeObject又能不能被回收呢?它將跟隨largeObject一塊兒始終存在,仍是和largeObject分離,獨立地被回收呢?ide

測試方法
帶着這個疑問,對現有的幾款現代javascript引擎分別進行了測試,參與測試的有:
~IE8自帶的JScript.dll
~IE9自帶的Chakra
~Opera 11.60自帶的Carakan
~Chrome 16.0.912.63自帶的V8(3.6.6.11)
~Firefox 9.0.1自帶的SpiderMonkey函數

測試的基本方案是,使用相似如下的代碼:工具

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function() {
        debugger;
    };
}
var inner = outer();

經過各瀏覽器的開發者工具(Developer Tools、Firebug、Dragonfly等),在斷點處中止javascript的執行,並經過控制檯或本地變量查看功能檢查largeObject的值,若是其值存在,則認爲GC並無回收該對象。測試

對於部分瀏覽器(特別是IE),考慮到對腳本執行有2種模式(執行模式和調試模式,IE經過開發者工具的Script面板中的「Start Debugging」按鈕切換),在調試模式下才會命中斷點,可是調試模式下可能存在不一樣的引擎優化方案,所以採用內存比對的方式進行測試。即打開資源瀏覽器,在var inner = outer();一行後強制執行一次垃圾回收(IE使用window.CollectGarbage();Opera使用window.opera.collect();),查看內存的變化。若是內存始終有100MB的佔用,沒有明顯的降低現象,則認爲GC並無回收該對象。優化

對於用例的設計,因爲從ECMAScript標準中能夠得知,全部的變量訪問是經過一個LexicalEnvironment對象進行的,所以目標在於在不一樣的LexicalEnvironment結構下進行測試。從標準中,搜索LexicalEnvironment不可貴出可以改變LexicalEnvironment結構的狀況有如下幾種:

1.進入一個函數。
2.進入一段eval代碼。
3.使用with語句。
4.使用catch語句。
所以如下將針對這4種狀況,進行多用例的測試。

測試過程級結果
基本測試
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function() {
        debugger;
    };
}
var inner = outer();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收,內存會恢復到outer函數執行前的狀態。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 回收,訪問largeObject拋出ReferenceError。
~SpiderMonkey – 回收,訪問largeObject獲得undefined。

結論
當一個函數outer返回另外一個函數inner時,Chakra、V8和SpiderMonkey會對outer中聲明,但inner中不使用的變量進行回收,其中V8直接將變量從LexicalEnvironment上解除綁定,而SpiderMonkey僅僅將變量的值設爲undefined,並不解除綁定。

多個變量的狀況
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var anotherLargeObject = LargeObject.fromSize('100MB');
 
    return function() {
        largeObject;
        debugger;
    };
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收anotherLargeObject,內存會回到outer調用前並增長100MB左右。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 回收,訪問largeObject能獲得正確的值,訪問anotherLargeObject拋出ReferenceError。
~SpiderMonkey – 回收,訪問largeObject能獲得正確的值,訪問anotherLargeObject獲得undefined。

結論
當一個LexicalEnvironment上存在多個變量綁定時,Chakra、V8和SpiderMonkey會針對不一樣的變量判斷是否有被使用,該判斷方法是掃描返回的函數inner的源碼來實現的,隨後會將沒有被inner使用的變量從LexicalEnvironment中解除綁定(一樣的,SpiderMonkey不解除綁定,僅賦值爲undefined),而剩下的變量繼續保留。

eval的影響
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function() {
        eval('');
        debugger;
    };
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 不回收,內存無降低趨勢。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 不回收,訪問largeObject可獲得正確的值。
~SpiderMonkey – 不回收,訪問largeObject可獲得正確的值。

結論
若是返回的inner函數中有使用eval函數,則不LexicalEnvironment中的任何變量進行解除綁定的操做,保留全部變量的綁定,以免產生不可預期的結果。

間接調用eval
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function() {
        window.eval('');
        debugger;
    };
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收,內存會恢復到outer函數執行前的狀態。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 回收,訪問largeObject拋出ReferenceError。
~SpiderMonkey – 回收,訪問largeObject獲得undefined。

結論
因爲ECMAScript規定間接調用eval時,代碼將在全局做用域下執行,是沒法訪問到largeObject變量的。所以對於間接調用eval的狀況,各javascript引擎將按標準的方式進行處理,無視該間接調用eval的存在。
一樣的,對於new Function(‘return largeObject;’)這種情形,因爲標準規定new Function建立的函數的[[Scope]]是全局的LexicalEnvironment,所以也沒法訪問到largeObject,全部引擎都參照間接調用eval的方式,選擇無視Function構造函數的調用。

多個嵌套函數
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    function help() {
        largeObject;
        // eval('');
    }
 
    return function() {
        debugger;
    };
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 不回收,內存無降低趨勢。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 不回收,訪問largeObject可獲得正確的值。
~SpiderMonkey – 不回收,訪問largeObject可獲得正確的值。

結論
不只僅是被返回的inner函數,若是在outer函數中定義的嵌套的help函數中使用了largeObject變量(或直接調用eval),也一樣會形成largeObject變量沒法回收。所以javascript引擎掃描的不只僅是inner函數的源碼,一樣掃描了其餘全部嵌套函數的源碼,以判斷是否能夠解除某個特定變量的綁定。

使用with表達式
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var scope = { o: LargeObject.fromSize('100MB') };
 
    with (scope) {
        return function() {
            debugger;
        };
    }
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收largeObject,但不回收scope.o,內存恢復至outer函數被調用前並增長100MB左右(沒法得知scope是否被回收)。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 不回收,訪問largeObject和scope以及o都可獲得正確的值。
~SpiderMonkey – 回收largeObject和scope,訪問該2個變量均獲得undefined,不回收o,可獲得正確的值。

結論
當有with表達式時,V8將會放棄全部變量的回收,保留LexicalEnvironment中全部變量的綁定。而SpiderMonkey則會保留由with表達式生成的新的LexicalEnvironment中的全部變量的綁定,而對於outer函數生成的LexicalEnvironment,按標準的方式進行處理,儘量解除其中的變量綁定。

使用catch表達式
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    try {
        throw { o: LargeObject.fromSize('100MB'); }
    }
    catch (ex) {
        return function() {
            debugger;
        };
    }
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收largeObject和ex,內存會恢復到outer函數被調用前的狀態。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 僅回收largeObject,訪問largeObject拋出ReferenceError,但仍可訪問到ex。
~SpiderMonkey – 僅回收largeObject,訪問largeObject獲得undefined,但仍可訪問到ex。

結論
catch表達式雖然會增長一個LexicalEnvironment,但對閉包內變量的綁定解除算法幾乎沒有影響,這源於catch生成的LexicalEnvironment僅僅是追加了被catch的Error對象一個綁定,是可控的(相對的with則不可控),所以對變量回收的影響也能夠控制和優化。但對於新生成並添加了Error對象的LexicalEnvironment,V8和SpiderMonkey均不會進一步優化回收,而Chakra則會對該LexicalEnvironment進行處理,若是其中的Error對象能夠回收,則會解除其綁定。

嵌套函數中聲明的同名變量
使用代碼

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
 
    return function(largeObject /* 或在函數體內聲明 */) {
        // var largeObject;
    };
}
var inner = outer();
inner();

測試結果
~JScript.dll – 不回收,內存無降低趨勢。
~Chakra – 回收,內存會恢復到outer函數被調用前的狀態。
~Carakan – 不回收,內存無降低趨勢。
~V8 – 回收,內存會恢復到outer函數被調用前的狀態。
~SpiderMonkey – 回收,內存會恢復到outer函數被調用前的狀態。

結論
嵌套函數中有與外層函數同名的變量或參數時,不會影響到外層函數中該變量的回收優化。即javascript引擎會排除FormalParameterList和全部VariableDeclaration表達式中的Identifier,再掃描全部Identifier來分析變量的可回收性。

整體結論
首先一個較爲明確的結論是,如下內容會影響到閉包內變量的回收:
~嵌套的函數中是否有使用該變量。
~嵌套的函數中是否有直接調用eval。
~是否使用了with表達式。

Chakra、V8和SpiderMonkey將受以上因素的影響,表現出不盡相同又較爲類似的回收策略,而JScript.dll和Carakan則徹底沒有這方面的優化,會完整保留整個LexicalEnvironment中的全部變量綁定,形成必定的內存消耗。

因爲對閉包內變量有回收優化策略的Chakra、V8和SpiderMonkey引擎的行爲較爲類似,所以能夠總結以下,當返回一個函數fn時:
1.若是fn的[[Scope]]是ObjectEnvironment(with表達式生成ObjectEnvironment,函數和catch表達式生成DeclarativeEnvironment),則:
A.若是是V8引擎,則退出全過程。
B.若是是SpiderMonkey,則處理該ObjectEnvironment的外層LexicalEnvironment。

2.獲取當前LexicalEnvironment下的全部類型爲Function的對象,對於每個Function對象,分析其FunctionBody:
A.若是FunctionBody中含有直接調用eval,則退出全過程。
B.不然獲得全部的Identifier。
C.對於每個Identifier,設其爲name,根據查找變量引用的規則,從LexicalEnvironment中找出名稱爲name的綁定binding。
D.對binding添加notSwap屬性,其值爲true。

3.檢查當前LexicalEnvironment中的每個變量綁定,若是該綁定有notSwap屬性且值爲true,則:
A.若是是V8引擎,刪除該綁定。
B.若是是SpiderMonkey,將該綁定的值設爲undefined,將刪除notSwap屬性。
對於Chakra引擎,暫沒法得知是按V8的模式仍是按SpiderMonkey的模式進行。

從以上測試及結論來看,V8確實是一個優秀的javascript引擎,在這一方面的優化至關到位。而SpiderMonkey則採起一種更爲友好的方式,不直接刪除變量的綁定,而是將值賦爲undefined,也許是SpiderMonkey團隊考慮到有一些極端特殊的狀況,依舊有可能致使使用到該變量,所以保證至少不會拋出ReferenceError打斷代碼的執行。而IE9的Chakra相比IE8的JScript.dll進步很是大,細節上的處理也很優秀。Opera的Carakan在這一方面則相對落後,徹底沒有對閉包內的變量回收進行優化,選擇了最爲穩妥但略顯浪費的方式。

此外,全部帶有優化策略的瀏覽器,都在內在開銷和速度之間選擇了一個平衡點,這也正是爲何「多個嵌套函數」這一測試用例中,雖然inner沒有再使用largeObject對象,甚至在inner中的斷點處,連help函數對象也已經解除綁定,卻沒有解除largeObject的綁定。基於這種現象,能夠推測各引擎均只選擇檢查一層的關聯性,即不去處理inner -> help -> largeObject這樣深度的引用關係,只找inner -> largeObject和help -> largeObject並作一個合集來處理,以提升效率。也許這種方式依舊存在內存開銷的浪費,但同時CPU資源也是很是貴重的,如何掌握這之間的平衡,即是javascript引擎的選擇。

此外,根據部分開發者的測試,Chakra甚至有資格被稱爲現有最快速的javascript引擎,微軟也一直在努力,而開發者更不該該一味地謾罵和嘲笑IE。咱們能夠嘲笑IE6的落後,能夠看不到低版本的IE曾經爲互聯網的發展作過的貢獻,能夠在這些歷史產品已經沒落的今天無情地給予打擊,卻最最不該該將整個IE系列一視同仁,掛上「垃圾」的名號。客觀地去看待,去評價,正是一個技術人員應該具有的最基本的準則和素養。

相關文章
相關標籤/搜索