理解javascript中的閉包
閱讀目錄
- 什么是閉包?
- 閉包的特性
- 閉包的作用:
- 閉包的代碼示例
- 注意事項
- 總結(jié)
閉包在javascript來說是比較重要的概念,平時工作中也是用的比較多的一項技術(shù)。下來對其進行一個小小的總結(jié)
什么是閉包?
官方說法:
閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù),通過另一個函數(shù)訪問這個函數(shù)的局部變量------《javascript高級程序設計第三版》
下面就是一個簡單的閉包:
function A(){ var text="hello world"; function B(){ console.log(text); } return B; } var c=A(); c(); // hello world
按照字面量的意思是:函數(shù)B有權(quán)訪問函數(shù)A作用域中的變量(text),通過另一個函數(shù)C來訪問這個函數(shù)的局部變量text。因此函數(shù)B形成了一個閉包。也可以說C是一個閉包,因為C執(zhí)行的實際是函數(shù)B。
這個需要注意的是,直接執(zhí)行A();是沒有任何反應的。因為return B沒有執(zhí)行,除非是return B();
閉包的特性
閉包有三個特性:
1.函數(shù)嵌套函數(shù)
2.函數(shù)內(nèi)部可以引用外部的參數(shù)和變量
3.參數(shù)和變量不會被垃圾回收機制回收
解釋一下第3點,為什么閉包的參數(shù)和變量不會被垃圾回收機制回收呢?
首先我們先了解一下javascript的垃圾回收原理:
(1)、在javascript中,如果一個對象不再被引用,那么這個對象就會被GC(garbage collection)回收;
(2)、如果兩個對象互相引用,而不再被第3者所引用,那么這兩個互相引用的對象也會被回收。
上面的示例代碼中A是B的父函數(shù),而B被賦給了一個全局變量C(全局變量的生命周期直至瀏覽器卸載頁面才會結(jié)束),這導致B始終在內(nèi)存中,而B的存在依賴于A,因此A也始終在內(nèi)存中,不會在調(diào)用結(jié)束后,被垃圾回收機制(garbage collection)回收。
閉包的作用:
其實閉包的作用也是有閉包的特性決定的,根據(jù)上面的閉包特性,閉包的作用如下:
1、可以讀取函數(shù)內(nèi)部的變量,而不是定義一起全局變量,避免污染環(huán)境
2、讓這些變量的值始終保持在內(nèi)存中。
閉包的代碼示例
下面主要介紹幾種常見的閉包,并進行解析:
demo1 局部變量的累加。
function countFn(){ var count=1; return function(){ //函數(shù)嵌套函數(shù) count++; console.log(count); } } var y = countFn(); //外部函數(shù)賦給變量y; y(); //2 //y函數(shù)調(diào)用一次,結(jié)果為2,相當于countFn()() y(); //3 //y函數(shù)調(diào)用第二次,結(jié)果為3,因為上一次調(diào)用的count還保存在內(nèi)存中,沒有被銷毀,所以實現(xiàn)了累加 y=null; //垃圾回收,釋放內(nèi)存 y(); // y is not a function
由于第一次執(zhí)行完,變量count還保存在內(nèi)存中,所以不會被回收,以致于第二次執(zhí)行的時候可以對上次的值就行累加。當引入y=null時,銷毀引用,釋放內(nèi)存
demo2 循環(huán)中使用閉包
代碼如下(下面的三個代碼示例):我們的目的是想在每次循環(huán)中調(diào)用循環(huán)序號:
demo2-1
for (var i = 0; i < 10; i++) { var a = function(){ console.log(i) } a() //依次為0--9 }
這個例子的結(jié)果是沒有題的,我們依次打印出了0-9
每一層匿名函數(shù)和變量i都組成了一個閉包,但是這樣在循環(huán)中并沒有問題,因為函數(shù)在循環(huán)體中立即被執(zhí)行了
demo2-2
但是在setTimeout中就不一樣了
for(var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); //10次10 }, 1000); }
我們期望的依次是打印出0--10,實際情況是打印出 10次10。即使吧setTimeout的時間改為0,也是打印出10個10。這是為什么呢?
這是因為setTimeout的一種機制,setTimeout是從任務隊列結(jié)束的時候開始計時的,如果前面有進程沒有結(jié)束,那么它就等到它結(jié)束再開始計時。在這里,任務隊列就是它自己所在的循環(huán)。
循環(huán)結(jié)束setTimeout才開始計時,所以無論如何,setTimeout里面的i都是最后一次循環(huán)的 i。該代碼中,最后的 i 為10,所以打印出了10個10.
這也就是為什么setTimeout的回調(diào)不是每次取循環(huán)時的值,而取最后一次的值
demo2-3
解決上面的setTimeout不能依次打印出循環(huán)的問題
for(var i=0;i<10;i++){ var a=function(e){ return function(){ console.log(e); //依次輸入0--9 } } setTimeout(a(i),0); }
因為setTimeout第一個參數(shù)需要一個函數(shù),所以返回一個函數(shù)給它,返回的同時把 i 作為參數(shù)傳進去,通過形參 e 緩存了i,也就是說e變量相當于是 i 的一個拷貝 ,并帶進返回的函數(shù)里面。
當 setTimeout 的執(zhí)行時,它就擁有了對 e 的引用,而這個值是不會被循環(huán)改變的。
也可以用下面的寫法,和上面類似:
for(var i = 0; i < 10; i++) { (function(e) { setTimeout(function() { console.log(e); //依次打印出0-9 }, 0); })(i); }
demo3 循環(huán)中添加事件
看下面的一個典型的demo.
我們希望每次點擊li的時候,alert出li的索引值,所以用下面的代碼:
<ul id="test"> <li>第一個</li> <li>第二個</li> <li>第三個</li> <li>第四個</li> </ul> var nodes = document.getElementsByTagName("li"); for(i = 0,len=nodes.length;i<len;i++){ nodes[i].onclick = function(){ alert(i); //值全是4 }; }
事與愿違,無論點擊哪一個li,都是alert(4),也就是都是alert循環(huán)結(jié)束之后的索引值。這是為什么呢?
這是因為循環(huán)中為不同的元素綁定事件,事件回調(diào)函數(shù)里如果調(diào)用了跟循環(huán)相關(guān)的變量,則這個變量取循環(huán)的最后一個值。
由于綁定的回調(diào)函數(shù)是一個匿名函數(shù),所以上面的代碼中, 這個匿名函數(shù)是一個閉包,攜帶的作用域為外層作用域(也就是for里面的作用域),當事件觸發(fā)的時候,作用域中的變量已經(jīng)隨著循環(huán)走到最后了。
還有一點就是,事件是需要觸發(fā)的,而絕大多數(shù)情況下,觸發(fā)的時候循環(huán)已經(jīng)結(jié)束了,所以循環(huán)相關(guān)的變量就是最后一次的取值。
要實現(xiàn)點擊li,alert出li的索引值,需要將上面的代碼進行以下的修改:
<ul id="test"> <li>第一個</li> <li>第二個</li> <li>第三個</li> <li>第四個</li> </ul> var nodes=document.getElementsByTagName("li"); for(var i=0;i<nodes.length;i++){ (function(e){ nodes[i].onclick=function(){ alert(e); }; })(i) }
解決思路: 增加若干個對應的閉包域空間(這里采用的是匿名函數(shù)),專門用來存儲原先需要引用的內(nèi)容(下標)。
當立即執(zhí)行函數(shù)執(zhí)行的時候,e 值不會被銷毀,因為它的里面有個匿名函數(shù)(也可以說是因為閉包的存在,所以變量不會被銷毀)。執(zhí)行后,e 值 與全局變量 i 的聯(lián)系就切斷了,
也就是說,執(zhí)行的時候,傳進的 i 是多少,立即執(zhí)行函數(shù)的 e 就是多少,但是 e 值不會消失,因為匿名函數(shù)的存在。
也可以用下面的解法,原理是一樣的:
<ul id="test"> <li>第一個</li> <li>第二個</li> <li>第三個</li> <li>第四個</li> </ul> var nodes=document.getElementsByTagName('li'); for(var i = 0; i<nodes.length;i++){ (function(){ var temp = i; nodes[i].onclick = function () { alert(temp); } })(); }
注意事項
1、造成內(nèi)存泄露
由于閉包會攜帶包含它的函數(shù)的作用域,因此會比其他函數(shù)占用更多的內(nèi)存。過度使用閉包可能會導致內(nèi)存占用過多,所以只有在絕對必要時再考慮使用閉包。
2、在閉包中使用this也可能會導致一些問題。
代碼示例:來源于《js高級程序設計3》;
其實我們的目的是想alert出object里面的name
var name="The Window"; var object={ name:"My Object", getNameFunc:function(){ return function(){ return this.name; } } } alert(object.getNameFunc()()); // The Window
因為在全局函數(shù)中,this等于window,而當函數(shù)被作為某個對象的方法調(diào)用時,this等于那個對象。不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其this對象通常指向window。
每個函數(shù)在被調(diào)用時,都會自動取的兩個特殊變量:this和 arguments。內(nèi)部函數(shù)在搜索這兩個變量時,只會搜索到其活動對象為止。也就是說,里面的return function只會搜索
到全局的this就停止繼續(xù)搜索了。因為它永遠不可能直接訪問外部函數(shù)中的這兩個變量。
稍作修改,把外部作用域中的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
我們把this對象賦值給了that變量。定義了閉包之后閉包也可以訪問這個變量。因此,即使在函數(shù)返回之后,that也仍引用這object,所以調(diào)用object.getNameFunc()()就返回 “My Object”了。
總結(jié)
當在函數(shù)內(nèi)部定義了其他函數(shù),就創(chuàng)建了閉包。閉包有權(quán)訪問包含函數(shù)內(nèi)部的所有變量。
閉包的作用域包含著它自己的作用域、包含函數(shù)的作用域和全局作用域。
當函數(shù)返回一個閉包時,這個函數(shù)的作用域會一直在內(nèi)存中保存到閉包不存在為止。
使用閉包必須維護額外的作用域,所有過度使用它們可能會占用大量的內(nèi)存
以上就是本文的全部內(nèi)容,希望本文的內(nèi)容對大家的學習或者工作能帶來一定的幫助,同時也希望多多支持腳本之家!
相關(guān)文章
深入理解javascript學習筆記(一) 編寫高質(zhì)量代碼
編寫高質(zhì)量JavaScript的一些要素,例如避免全局變量,使用單變量聲明,在循環(huán)中預緩存length(長度),遵循代碼閱讀,以及更多2012-08-08javascript HTML5 canvas實現(xiàn)打磚塊游戲
這篇文章主要介紹了基于javascript HTML5 canvas實現(xiàn)打磚塊游戲的具體實現(xiàn)代碼,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-04-04JavaScript實現(xiàn)表格表單的隨機選擇和簡單的隨機點名
本文主要介紹了JavaScript實現(xiàn)表格表單的隨機選擇和簡單的隨機點名,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-08-08