作用域
作用域指的是變量的有效訪問范圍。作用域對Javascript有重要意義,了解作用域的工作原理是在性能角度和功能角度理解Javascript的關鍵。
每一個JavaScript函數都被表示為對象,是一個函數實例。以下兩種定義函數的方式是等價的。
var sayName = function(){ alert('hello world!'); } var sayName = new Function('alert("hello world!")');
函數對象正如其他對象那樣,擁有可以被Javascript代碼訪問的屬性,和一系列不能被Javascript代碼訪問,僅供JavaScript引擎使用的內部屬性。其中一個內部屬性是[[Scope]]。
內部[[Scope]]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定哪些數據可由函數訪問。此函數作用域鏈中的每個對象被稱為一個可變對象,每個可變對象都以“鍵值對”的形式存在。當一個函數創建后,它的作用域鏈被填充以對象,這些對象代表創建此函數的環境中可訪問的數據。例如下面這個全局函數:
function add(num1, num2) { var sum = num1 + num2; return sum; }
當add()函數創建后,它的內部屬性指向一個作用域鏈,該作用域鏈中被填入一個單獨的可變對象,這個可變對象是一個全局對象。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。

而當add函數運行時會建立一個內部對象,稱作“運行期上下文”。一個運行期上下文定義了一個函數運行時的環境。對函數的每次運行而言,每個運行期上下文都是獨一的,所以多次調用同一個函數就會導致多次創建運行期上下文(execution context)。當函數執行完畢,運行期上下文就被銷毀。
一個運行期上下文有它自己的作用域鏈,用於標識符解析。當運行期上下文被創建時,它的作用域鏈被初始化。包括函數的[[Scope]]屬性中所包含的對象會按照它們出現在作用域鏈中的順序,被復制到運行期上下文的作用域鏈中。這項工作一旦完成,一個被稱作“激活對象”的新對象就為運行期上下文創建好了。此激活對象作為函數執行期的一個可變對象,包含訪問所有局部變量,命名參數,參數集合,和this的接口。然后,此對象被推入作用域鏈的前端。當作用域鏈被銷毀時,激活對象也一同銷毀。下圖顯示了add函數運行時所對應的運行期上下文和它的作用域鏈。
var total = add(5, 10);

需要記住的是兩點:
- [[scope]]屬性是函數創建到運行時一直存在的,知道函數被銷毀后占用的內存才會被釋放
- 運行期上下文只存在於函數運行期間,函數運行結束后該對象被銷毀
閉包、內存泄露
閉包是JavaScript最強大的一個方面,它允許函數訪問局部范圍之外的數據。
function assignEvents() { var id = "xdi9592"; document.getElementById('save').onclick = function(event){ saveDocument(id); } }
當assignEvents()被執行時,一個激活對象被創建,並包含了該函數作用域內所有可訪問的變量和函數,其中包括id變量。它將成為運行期上下文作用域鏈上的第一個對象,全局對象是第二個。當閉包創建時,[[Scope]]屬性包含了作用域內所有對象的集合(等於assignEvents運行期上下文的作用域鏈,即assignEvents的激活對象,全局對象)。

由於閉包的[[Scope]]屬性包含與運行期上下文作用域鏈相同的對象引用,會產生副作用。通常,一個函數的激活對象與運行期上下文一同銷毀。當涉及閉包時,運行期上下文對象,以及他的作用域鏈被銷毀,但激活對象就無法銷毀,因為引用仍然存在於閉包的[[Scope]]屬性中。除非手動接觸所有對匿名函數的引用,等到垃圾收集器下次運行時,assignEvents的激活對象才會隨着匿名函數一同被銷毀。
document.getElementById('save').onclick = null;
所以在Javascript代碼中閉包與非閉包函數相比,需要更多內存開銷。在大型網頁應用中,這可能會導致內存泄露與難以排查的性能問題。
隨着瀏覽器的升級,大部分瀏覽器對於閉包引起的循環引用問題都能夠順利解決。但IE9之前使用非本地JavaScript對象實現DOM對象,對於Javascript對象跟DOM對象使用不同的垃圾收集器。所以閉包在IE的這些版本中發生循環引用時便會導致內存泄露。
