要想理解閉包,應當先理解JavaScript的作用域和作用域鏈。
JavaScript有一個特性被稱之為“聲明提前(hoisting)”,即JavaScript函數裡聲明的所有變量(但不涉及賦值)都被“提前”至函數體的頂部,“聲明提前”這步操作是在JavaScript引擎的“預編譯”時進行的,是在代碼開始運行之前,看一看下面的例子:
var name = "YY"; function getName(){ console.log(name); //輸出undefine,而不是“YY” var name = "Crucify"; console.log(name); //輸出“Crucify” }
首先局部變量定義了一個和全局變量相同名字的變量,則在函數體內部局部變量遮蓋了同名的全局變量,然後在函數體內部變量name的聲明被提前至函數體頂部但並沒有賦值,所以此時name是一個只被聲明但並沒有初始化的變量,我們知道變量只進行聲明但並不初始化則它的值為undefine,所以第一行打印時undefine,下一行開始為變量name進行賦值,所以第二行打印的輸出是我們所期望的。
當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。
執行環境(execution context)定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。
作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。當JavaScript需要查找變量x的值的時候(這個過程稱作“變量解析”(variable resolution)),他會從列表之中的第一個對象開始查找直到最後一個對象,如果某個對象有一個名為x的屬性,則會直接食用這個屬性的值。如果作用域鏈上沒有任何一個對象含有屬性x,那麼就認為這段代碼的作用域上不存在x,並最終拋出一個引用錯誤異常。
所謂的“變量對象的指針列表”很好理解。在JavaScript的最頂層代碼中(也就是不包含在任何函數定義內的代碼),作用域鏈是由一個全局對象組成。在不包含嵌套的函數體內,作用域鏈上有兩個對象,第一個是定義函數參數和局部變量的對象,第二個是全局對象。在一個嵌套的函數體內,作用域鏈上則至少有三個對象,看下面的例子:
var name = "YY"; function getName(){ var name = "Crucify"; function f(){ return name; } return f(); }
函數f()的作用域鏈上有三個對象,第一個是定義函數f()參數和局部變量的對象,第二個是定義函數getName()參數和局部變量的對象,第三個是全局對象。
每個環境都可以向上搜索作用域鏈,以查詢變量和函數名,但任何環境都不能通過向下搜索作用域鏈而進入另一個執行環境,即函數f()可以向上搜索函數getName()和全局對象中的屬性,但是全局對象不能向下搜索getName()和f()中的值。
而創建閉包的常見方式,就是在一個函數內部創建另一個函數。閉包是指有權訪問另一個函數作用域中的變量的函數。
一般來講,當函數執行完畢後,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況有所不同,因為在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中,參考下面的代碼:
function createComparisonFunction(propertyName) { return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if (value1 < value2){ return -1; } else if (value1 > value2){ return 1; } else { return 0; } }; }
當下列代碼執行時,包含函數與內部匿名函數的作用域鏈如圖所示:
var compare = createComparisonFunction("name"); var result = compare({ name: "Nicholas" }, { name: "Greg" });
當createComparisonFunction()函數在執行完畢後,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。直到匿名函數被銷毀後, createComparisonFunction()的活動對象才會被銷毀:
compare = null; //解除對匿名函數的引用(以便釋放內存)
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多,所以在絕對必要時再考慮使用閉包。
作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最後一個值。因為閉包所保存的是整個變量對象,而不是某個特殊的變量:
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; }
這個函數會返回一個函數數組,且每個函數都返回 10。
在閉包中使用 this 對象也可能會導致一些問題。我們知道, this 對象是在運行時基於函數的執行環境綁定的,而匿名函數的執行環境具有全局性,因此其 this 對象通常指向 window(在通過 call()或 apply()改變函數執行環境的情況下, this 就會指向其他對象),看下面的例子:
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //"The Window"(在非嚴格模式下)
把外部作用域中的 this 對象保存在一個閉包能夠訪問到的變量裡,就可以讓閉包訪問該對象了,如下所示:
var name = "The Window"; var object = { name : "My Object", getNameFunc : function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()()); //"My Object"
arguments 也存在同樣的問題。如果想訪問作用域中的 arguments 對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。
JavaScript高級程序設計(第3版)高清完整PDF中文+英文+源碼 http://www.linuxidc.com/Linux/2014-09/107426.htm
如何使用JavaScript書寫遞歸函數 http://www.linuxidc.com/Linux/2015-01/112000.htm
JavaScript核心概念及實踐 高清PDF掃描版 (邱俊濤) http://www.linuxidc.com/Linux/2014-10/108083.htm
理解JavaScript中的事件流 http://www.linuxidc.com/Linux/2014-10/108104.htm