這幾天看到閉包一章,從工具書到各路大神博客,都各自有着不一樣的理解,如下我將選擇性的抄(咳咳,固然仍是會附上本身理解的)一些大神們對閉包的原理及其使用文章,看成是本身初步理解這一功能函數的過程吧。javascript
首先先上連接:html
還有一些也很不錯,但主要是以應用爲主,原理解釋沒有上面幾篇深刻,不過做爲閉包的拓展應用其實也能夠看一看;前端
閉包是指有權訪問另外一個函數做用域中的變量的函數。建立閉包的常見方式,就是在一個函數內部建立另外一個函數。
從這句話咱們知道:閉包是一個函數git
function createComparisonFunction(propertyName) { return function(obj1,obj2) { var value1 = obj1[propertyName]; var value2 = obj2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } }; }
這段代碼,咱們能直接看出,共存在三個做用域,Global、createComparisonFunction、匿名函數funciton,因其JS的做用域鏈特性,後者能訪問自身及前者的做用域。而返回的匿名函數即便在其餘地方被調用了,但它仍能夠訪問變量propertyName。之因此還可以訪問這個變量,是由於內部函數的做用域鏈中包含createComparisonFunction的做用域。咱們來深刻了解一下,函數執行時具體發生了什麼?github
當第一個函數被調用時,會建立一個執行環境(Execution Context,也叫執行上下文)及相應的做用域鏈,並把做用域鏈賦值給一個特殊的內部屬性[[Scope]]。而後,使用this、arguments和其餘命名參數的值來初始化函數的活動對象(Activation Object)。但在做用域鏈中,外部函數的活動對象處於第二位,外部函數的外部函數處於第三位,最後是全局執行環境(Global Context)。面試
換一個栗子:chrome
function createFunctions() { var result = new Array(); for (var i=0;i<10;i++) { result[i] = function() { return i; }; } return result; } var arr = createFunctions(); alert(arr[0]()); // 10 alert(arr[1]()); // 10
/這個函數返回一個函數數組。表面上看,彷佛每一個函數都應該返回本身的索引值,位置0的函數返回0,位置1的函數返回1,以此類推。但但實際上,每一個函數都返回10,爲何?
數組對象內的匿名函數裏的i是引用createFunctions做用域內的,當調用數組內函數的時候,createFunctions函數早已執行完畢。編程
這圖不傳也罷了,畫得忒醜了。
數組內的閉包函數指向的i,存放在createFunctions函數的做用域內,確切的說,是在函數的變量對象裏,for循環每次更新的i值,就是從它那兒來的。因此當調用數組函數時,循環已經完成,i也爲循環後的值,都爲10;數組
有人會問,那result[i]爲何沒有變爲10呢?
要知道,做用域的斷定是看是否在函數內的,result[i] = function.......
是在匿名函數外,那它就仍是屬於createFunctions
的做用域內,那result[i]
裏的i就依然會更新
那麼如何使結果變爲咱們想要的呢?也是經過閉包。
function createFunctions() { var result = []; for (var i=0;i<10;i++) { !function(i) { result[i] = function() {console.log(i)}; }(i); } return result; } var arr = createFunctions(); arr[0](); arr[1](); arr[2]();
function createFunctions() { var result = []; function fn(i) { result[i] = function() {console.log(i)} }; for (var i=0;i<10;i++) { fn(i); } return result; } var arr = createFunctions(); arr[0](); arr[1](); arr[2]();
var arr = []; function fn(i) { arr[i] = function() {console.log(i)} } function createFunctions() { for (var i=0;i<10;i++) { fn(i); } } fn(createFunctions()); arr[0](); arr[1](); arr[2]();
以第一種爲例,經過一個當即調用函數,將外函數當前循環的i
值做爲實參傳入,並存放在當即調用函數的變量對象內,此時,這個函數當即調用函數和數組內的匿名函數就至關於一個閉包,數組的匿名函數引用了當即調用函數變量對象內的i。當createFuncions
執行完畢,裏面的i值已是10了。可是因爲閉包的特性,每一個函數都有各自的i值對應着。對數組函數而言,至關於產生了10個閉包。
因此能看出,閉包也十分的佔用內存,只要閉包不執行,那麼變量對象就沒法被回收,因此不是特別須要,儘可能不使用閉包。
this
對象在閉包中使用this對象也會致使一些問題。咱們知道,this對象是在運行時基於函數的執行環境綁定的;在全局對象中,this等於window,而當函數被做爲某個對象的方法調用時,this等於那個對象。不過,匿名函數的執行環境具備全局性,所以其this對象一般指向window。但有時候因爲編寫閉包的方式不一樣,這一點可能不會那麼明顯。(固然能夠用call和apply)
var name = "The Window"; var obj = { name:"My Object", getName:function () { var bibao = function () { return this.name; }; return bibao; } }; alert(obj.getName()()); // The Window
先建立一個全局變量name,又建立一個包含name屬性的對象。這個對象包含一個方法——getName(),它返回一個匿名函數,而匿名函數又返回this.name。因爲getName()返回一個函數,所以調用obj.getName()();就會當即調用它返回的函數,結果就是返回一個字符串。然而,這個例子返回的字符串是"The Window",即全局name變量的值。爲何匿名函數沒有取得其波包含做用域(或外部做用域)的this對象呢?
每一個函數調用時其活動對象都會自動取得兩個特殊變量:this
和arguments
。
內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部做用域中的this對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。
var name = "The Window"; var obj = { name:"My Object", getName:function () { var that = this; return function () { return that.name; }; } }; alert(obj.getName()());
this
和arguments
也存在一樣的問題,若是想訪問做用域中arguments對象,必須將該對象的引用保存到另外一個閉包可以訪問的變量中。var name = "The Window"; var obj = { name:"My Object", getName:function (arg1,arg2) { var arg = []; arg[0] = arg1; arg[1] = arg2; function bibao() { return arg[0]+arg[1]; } return bibao; } }; alert(obj.getName(1,2)())obj.getName方法保存了其接收到的實參在它的變量對象上,並在執行函數結束後沒有被回收,由於返回的閉包函數引用着obj.Name方法裏的arg數組對象。使得外部變量成功訪問到了函數內部做用域及其局部變量。
在幾種特殊狀況下,this引用的值可能會意外的改變。
var name = "The Window"; var obj = { name:"My Object", getName:function () { return this.name; } };
這裏的getName()只簡單的返回this.name的值。
var name = "The Window"; var obj = { name:"My Object", getName:function () { console.log(this.name); } }; obj.getName(); // "My Object" (obj.getName)(); // "My Object" (obj.getName = obj.getName)(); // "The Window"
第一個obj.getName
函數做爲obj
對象的方法調用,則天然其this
引用指向obj
對象。
第二個,加括號將函數定義以後,做爲函數表達式執行調用,this
引用指向不變。
第三個,括號內先執行了一條賦值語句,而後在調用賦值後的結果。至關於從新定義了函數,this
引用的值不能維持,因而返回"The Window"。
setTimeout()
用setTimeout
結合循環考察閉包是一個很老的面試題了
// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
setTimeout
的執行與咱們日常的JS代碼執行不同,這裏須要提到一個隊列數據結構執行的概念。我的理解:因爲setTimeout
函數的特殊性,須等其餘非隊列結構代碼執行完畢後,這個setTimeout
函數纔會進入隊列執行棧。
setTimeout(function() { console.log(a); }, 0); var a = 10; console.log(b); console.log(fn); var b = 20; function fn() { setTimeout(function() { console.log('setTImeout 10ms.'); }, 10); } fn.toString = function() { return 30; } console.log(fn); setTimeout(function() { console.log('setTimeout 20ms.'); }, 20); fn();
答案:
設置斷點如圖所示,今天剛學Chrome的開發者工具,有哪些使用上的錯誤還請指出。
我分別給變量a、b、fn函數都設置了觀察,變量的值變化將會實時地在右上角中顯示,能夠看到,在JS解釋器運行第一行代碼前,變量a、b就已經存在了,而fn函數已經完成了聲明。接下來咱們繼續執行。要注意:藍色部分說明這些代碼將在下一次操做中執行,而不是已經執行完畢。
把第一個setTimeout函數執行完畢後也沒有反應。我給三個setTimeout內的匿名函數也加上觀察選項,卻顯示不可以使用。
因此,下一次執行會發生什麼?對console出b的值,可是b沒賦值,右上角也看到了,因此顯示undefined。
而console.log(fn)就是將fn函數函數體從控制檯彈出,要注意,console會隱式調用toString方法,這個會在後面講到。
如今第26行以前(不包括26行)的代碼都已略過,a,b變量也已獲得賦值,繼續執行。
重寫了toString
方法前:
重寫後:
toString方法是Object全部,全部由它構造的實例都能調用,如今這個方法被改寫並做爲fn對象的屬性(方法)保留下來。
console會隱式調用toString方法,因此30行的console會彈出30;
繼續執行,定義setTimeout函數也是什麼沒有發生,知道調用fn前。
調用fn,是否是就會執行setTimeout函數呢?其實沒有,咱們能夠看到call stack一欄已是fn的執行棧了,可是依舊沒發生什麼。
可是:
當call stack裏的環境都已退出,執行棧裏沒有任何上下文時,三個setTimeout函數就執行了,那這三個時間戳函數那個先執行,那個後執行呢?由設定的延遲時間決定,這個延遲時間是相對於其餘代碼執行完畢的那一刻。
不信咱們能夠經過改變延遲時間從新試一次就知道了。
咱們在看回原來的閉包代碼:
// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
先確認一個問題,setTimeout
函數裏的匿名函數的i指向哪兒?對,是全局變量裏的i。setTimeout
裏的匿名函數執行前,外部循環已經結束,i值已經更新爲6,這時setTimeout
調用匿名函數,裏面的i固然都是6了。
咱們須要建立一個可以保存當前i值的"盒子"給匿名函數,使得匿名函數可以引用新建立的父函數。
// 利用閉包,修改下面的代碼,讓循環輸出的結果依次爲1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { !function (i) { setTimeout( function timer() { console.log(i); }, i*1000 ); }(i); }
自調用函數就是那個"盒子"
考慮這個函數:
function f(arg) { var n = 123 + Number(arg); function g() {console.log("n is "+n);console.log("g is called");} n++; function gg() {console.log("n is "+n);console.log("g is called");} return [g,gg]; }
調用數組內函數的console結果是什麼?
var arr = f(1); arr[0](); // 對閉包g的調用 // "n is 125" "g is called" arr[1](); // 對閉包gg的調用 // "n is 125" "gg is called"
函數g與函數gg保持了各自含有局部變量n的執行環境。因爲聲明函數g時與聲明函數gg時的n值是不一樣的,所以閉包g與閉包gg貌似將會表示各自不一樣的n值。實際上二者都將表示相同的值。由於它們引用了同一個對象。
即都是引用了,f
函數執行環境內變量對象內的n
值。當執行f(1)
的時候,n值就已經更新爲最後計算的值。
在JavaScript中,最外層代碼(函數以外)所寫的名稱(變量名與函數名)具備全局做用域,即所謂的全局變量與全局函數。JavaScript的程序代碼即便在分割爲多個源文件後,也能相互訪問其全局名稱。在JavaScript的規範中不存在所謂的模塊的語言功能。
所以,對於客戶端JavaScript,若是在一個HTML文件中對多個JavaScript文件進行讀取,則他們相互的全局名稱會發生衝突。也就是說,在某個文件中使用的名稱沒法同時在另外一個文件中使用。
即便在獨立開發中這也很不方便,在使用他們開發的庫之類時就更加麻煩了。
此外,全局變量還下降了代碼的可維護性。不過也不能就簡單下定論說問題只是由全局變量形成的。這就如同在Java這種語言規範並不支持全局變量的語言中,一樣能夠很容易建立出和全局變量功能相似的變量。
也就是說,不該該只是一昧地減小全局變量的使用,而應該造成一種儘量避免使用較廣的做用域的意識。對於較廣的做用域,其問題在於修改了某處代碼以後,會難以肯定該修改的影響範圍,所以代碼的可維護性會變差。
從形式上看,在JavaScript中減小全局變量的數量的方法時很簡單的。首先咱們按照下面的代碼這樣預設一下全局函數與全局變量。
// 全局函數 function sum(a,b) { return Number(a)+Number(b); } // 全局變量 var position = {x:2,y:3}; // 藉助經過對象字面量生成對象的屬性,將名稱封入對象的內部。因而從形式上看,全局變量減小了 var MyModule = { sum:function (a,b) { return Number(a)+Number(b); }, position:{x:2,y:3} }; alert(MyModule.sum(3,3)); // 6 alert(MyModule.position.x); // 2
上面的例子使用對象字面量,不過也能夠像下面這樣不使用對象字面量。
var MyModule = {}; // 也能夠經過new表達式生成 MyModule.sum = function (a,b) {return Number(a)+Number(b);}; MyModule.position = {x:2,y:3};
這個例子中,咱們將MyModule稱爲模塊名。若是徹底採用這種方式,對於1個文件來講,只須要一個模塊名就能消減全局變量的數量。固然,模塊名之間仍然可能產生衝突,不過這一問題在其餘程序設計語言中也是一個沒法被避免的問題。
經過這種將名稱封入對象之中的方法,能夠避免名稱衝突的問題。可是這並無解決全局名稱的另外一個問題,也就是做用域過廣的問題。經過MyModule.position.x這樣一個較長的名稱,就能夠從代碼的任意一處訪問該變量。
// 在此調用匿名函數 // 因爲匿名函數的返回值是一個函數,因此變量sum是一個函數 var sum = (function () { // 沒法從函數外部訪問該名稱 // 實際上,這變成了一個私有變量 // 通常來講,在函數被調用以後該名稱就沒法再被訪問 // 不過因爲是在被返回的匿名函數中,因此仍能夠繼續被使用 var p = {x:2,y:3}; // 一樣是一個從函數外沒法被訪問的私有變量 // 將其命名爲sum也能夠。不過爲了不混淆,這裏採用其餘名稱 function sum_internal(a,b) { return Number(a)+Number(b); } // 只不過是爲了使用上面的兩個名稱而隨意設計的返回值 return function (a,b) { alert("x = "+p.x); return sum_internal(a,b); } })(); console.log(sum(3,4)); // "x = 2" // "y"
上面的代碼能夠抽象爲下面這種形式的代碼。在利用函數做用域封裝名稱,以及閉包可使名稱在函數調用結束後依然存在這兩個特性。這樣信息隱藏得以實現。
(function(){函數體})();
像上面這樣,當場調用函數的代碼看起來或許有些奇怪。通常的作法是先在某處聲明函數,以後在須要時調用。不過這種作法是JavaScript的一種習慣用法,加以掌握。
匿名函數的返回值是一個函數,不過即便返回值不是函數,也一樣能採用這一方法。好比返回一個對象字面量以實現信息隱藏的功能。
var obj = (function() { // 從函數外部沒法訪問該名稱 // 實際上,這是一個私有變量 var p = {x:2,y:3}; // 這一樣是一個沒法從函數外部訪問的私有函數 function sum_internal(a,b) { return Number(a+b); } // 只不過爲了使用上面的兩個名稱而隨意設計的返回值 return { sum:function (a,b) { return sum_internal(a,b); }, x:p.x }; })(); alert(obj.sum(3,4)); // 7 alert(obj.x); // 2
利用函數做用域與閉包,能夠實現訪問在控制,上一節中,模塊的函數在被聲明以後當即就對其調用,而是用了閉包的類則可以在生成實例時調用。即使如此,着厚重那個作法在形式上仍然只是單純的函數生命。下面是一個經過閉包來對類進行定義的例子
// 用於生成實例的函數 function myclass(x,y) { return {show:function () {alert(x+" | "+y)}}; } var obj = myclass(3,2); obj.show(); // 3 | 2
這裏再舉一個具體的例子,一個實現了計數器功能的類。
這裏重申一下:JavaScript的語言特性沒有"類"的概念。但這裏的類指的是,實際上將會調用構造函數的Function對象。此外在強調對象是經過調用構造函數生成的時候,會將這些被生成的對象稱做對象實例以示區別。
JavaScript有一種自帶的加強功能,稱爲支持函數型程序設計的表達式閉包(Expression closure
)。
從語法結構上看,表達式閉包是函數聲明表達式的一種省略形式。能夠像下面這樣省略只有return
的函數聲明表達式中的return
與{}
。
var sum = function (a,b) {return Number(a+b)}; // 能夠省略爲 var sum = function (a,b) Number(a+b);