全面理解JavaScript中的閉包
引子
閉包是有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)。
閉包是javascript中很難理解的部分,很多高級(jí)的應(yīng)用都依靠閉包來實(shí)現(xiàn)的,我們先來看下面的一個(gè)例子:
function outer() { var i = 100; function inner() { console.log(i); } }
上面代碼,根據(jù)變量的作用域,函數(shù)outer中所有的局部變量,對(duì)函數(shù)inner都是可見的;函數(shù)inner中的局部變量,在函數(shù)inner外是不可見的,所以在函數(shù)inner外是無法讀取函數(shù)inner的局部變量的。
既然函數(shù)inner可以讀取函數(shù)outer的局部變量,那么只要將inner作為返會(huì)值,就可以直接在ouer外部讀取inner的局部變量。
function outer() { var i = 100; function inner() { console.log(i); } return inner; } var rs = outer(); rs();
這個(gè)函數(shù)有兩個(gè)特點(diǎn):
- 函數(shù)inner嵌套在函數(shù)ouer內(nèi)部;
- 函數(shù)outer返回函數(shù)inner。
這樣執(zhí)行完var rs = outer()后,實(shí)際rs指向了函數(shù)inner。這段代碼其實(shí)就是一個(gè)閉包。也就是說當(dāng)函數(shù)outer內(nèi)的函數(shù)inner被函數(shù)outer外的一個(gè)變量引用的時(shí)候,就創(chuàng)建了一個(gè)閉包。
作用域
簡單的說,作用域就是變量與函數(shù)的可訪問范圍,即作用域控制著變量與函數(shù)的可見性和生命周期。在JavaScript中,變量的作用域有全局作用域和局部作用域兩種。
全局作用域
var num1 = 1; function fun1 (){ num2 = 2; }
以上三個(gè)對(duì)象num1,num2和fun1均是全局作用域,這里要注意的是末定義直接賦值的變量自動(dòng)聲明為擁有全局作用域;
局部作用域
function wrap(){ var obj = "我被wrap包裹起來了,wrap外部無法直接訪問到我"; function innerFun(){ //外部無法訪問我 } }
作用域鏈
Javascript中一切皆對(duì)象,這些對(duì)象有一個(gè)[[Scope]]屬性,該屬性包含了函數(shù)被創(chuàng)建的作用域中對(duì)象的集合,這個(gè)集合被稱為函數(shù)的作用域鏈(Scope Chain),它決定了哪些數(shù)據(jù)能被函數(shù)訪問。
function add(a,b){ return a+b; }
當(dāng)函數(shù)創(chuàng)建的時(shí)候,它的[[scope]]屬性自動(dòng)添加好全局作用域
var sum = add(3,4);
當(dāng)函數(shù)調(diào)用的時(shí)候,會(huì)創(chuàng)建一個(gè)稱為運(yùn)行期上下文(execution context)的內(nèi)部對(duì)象,z這個(gè)對(duì)象定義了函數(shù)執(zhí)行時(shí)的環(huán)境。它也有自己的作用域鏈,用于標(biāo)識(shí)符解析,而它的作用域鏈初始化為當(dāng)前運(yùn)行函數(shù)的[[Scope]]所包含的對(duì)象。
在函數(shù)執(zhí)行過程中,每遇到一個(gè)變量,都會(huì)經(jīng)歷一次標(biāo)識(shí)符解析過程以決定從哪里獲取和存儲(chǔ)數(shù)據(jù)。該過程從作用域鏈頭部,也就是從活動(dòng)對(duì)象開始搜索,查找同名的標(biāo)識(shí)符,如果找到了就使用這個(gè)標(biāo)識(shí)符對(duì)應(yīng)的變量,如果沒找到繼續(xù)搜索作用域鏈中的下一個(gè)對(duì)象,如果搜索完所有對(duì)象(最后一個(gè)為全局對(duì)象)都未找到,則認(rèn)為該標(biāo)識(shí)符未定義。
閉包
閉包簡單來說就是一個(gè)函數(shù)訪問了它的外部變量。
var quo = function(status){ return { getStatus: function(){ return status; } } }
status保存在quo中,它返回了一個(gè)對(duì)象,這個(gè)對(duì)象里的方法getStatus引用了這個(gè)status變量,即getStatus函數(shù)訪問它的外部變量status;
var newValue = quo('string');//返回了一個(gè)匿名對(duì)象,被newValue引用著 newValue.getStatus();//訪問到了quo的內(nèi)部變量status
假如并沒有g(shù)etStatus這個(gè)方法,那么quo('sting')結(jié)束后,status自動(dòng)被回收,正是因?yàn)榉祷氐哪涿麑?duì)象被一個(gè)全局對(duì)象引用,那么這個(gè)匿名對(duì)象又依賴于status,所以會(huì)阻止status的釋放。
例子:
//錯(cuò)誤方案 var test = function(nodes){ var i ; for(i = 0;i<nodes.length;i++){ nodes[i].onclick = function(e){ alert(i); } } }
匿名函數(shù)創(chuàng)建了一個(gè)閉包,那么其訪問的i是外部test函數(shù)中的i,所以每一個(gè)節(jié)點(diǎn)實(shí)際上引用的是同一個(gè)i。
//改進(jìn)方案 var test = function(nodes){ var i ; for(i = 0;i<nodes.length;i++){ nodes[i].onclick = function(i){ return function(){ alert(i); }; }(i); } }
每一個(gè)節(jié)點(diǎn)綁定了一個(gè)事件,這個(gè)事件接收一個(gè)參數(shù),并且立即運(yùn)行,傳入i,因?yàn)槭前粗祩鬟f的,所以每一次循環(huán)都會(huì)為當(dāng)前i產(chǎn)生一個(gè)新的備份。
閉包的作用
function outer() { var i = 100; function inner() { console.log(i++); } return inner; } var rs = outer(); rs(); //100 rs(); //101 rs(); //102
上面的代碼中,rs是閉包inner函數(shù)。rs共運(yùn)行了三次,第一次100,第二次101,第三次102,這說明在函數(shù)outer中的局部變量i一直保存在內(nèi)存中,并沒有在調(diào)用自動(dòng)清除。
閉包的作用就是在outer執(zhí)行完畢并返回后,閉包使javascript的垃圾回收機(jī)制(grabage collection)不會(huì)回收outer所占的內(nèi)存,因?yàn)閛uter的內(nèi)部函數(shù)inner的執(zhí)行要依賴outer中的變量。(另一種解釋:outer是inner的父函數(shù),inner被賦給了一個(gè)全局變量,導(dǎo)致inner會(huì)一直在內(nèi)存中,而inner的存在依賴于outer,因些outer也始終于在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后被垃圾收集回收)。
閉包有權(quán)訪問函數(shù)內(nèi)部的所有變量。
當(dāng)函數(shù)返回一個(gè)閉包時(shí),這個(gè)函數(shù)的作用域?qū)?huì)一直在內(nèi)存中保存到閉包不存在為止。
閉包與變量
由于作用域鏈的機(jī)制,閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值??聪旅胬樱?/p>
function f() { var rs = []; for (var i=0; i <10; i++) { rs[i] = function() { return i; }; } return rs; } var fn = f(); for (var i = 0; i < fn.length; i++) { console.log('函數(shù)fn[' + i + ']()返回值:' + fn[i]()); }
函數(shù)會(huì)返回一個(gè)數(shù)組,表面上看,似乎每個(gè)函數(shù)都應(yīng)該返回自己的索引值,實(shí)際上,每個(gè)函數(shù)都返回10,這是因?yàn)榈趥€(gè)函數(shù)的作用域鏈上都保存著函數(shù)f的活動(dòng)對(duì)象,它們引用的都是同一變量i。當(dāng)函數(shù)f返回后,變量i的值為10,此時(shí)每個(gè)函數(shù)都保存著變量i的同一個(gè)變量對(duì)象。我們可以通過創(chuàng)建另一個(gè)匿名函數(shù)來強(qiáng)制讓閉包的行為符合預(yù)期。
function f() { var rs = []; for (var i=0; i <10; i++) { rs[i] = function(num) { return function() { return num; }; }(i); } return rs; } var fn = f(); for (var i = 0; i < fn.length; i++) { console.log('函數(shù)fn[' + i + ']()返回值:' + fn[i]()); }
這個(gè)版本中,我們沒有直接將閉包賦值給數(shù)組,而是定義了一個(gè)匿名函數(shù),并將立即執(zhí)行匿名函數(shù)的結(jié)果賦值給數(shù)組。這里匿名函數(shù)有一個(gè)參數(shù)num,在調(diào)用每個(gè)函數(shù)時(shí),我們傳入變量i,由于參數(shù)是按值傳遞的,所以就會(huì)將變量i復(fù)制給參數(shù)num。而在這個(gè)匿名函數(shù)內(nèi)部,又創(chuàng)建了并返回了一個(gè)訪問num的閉包,這樣,rs數(shù)組中每個(gè)函數(shù)都有自己num變量的一個(gè)副本,因此就可以返回不同的數(shù)值了。
閉包中的this對(duì)象
var name = 'Jack'; var o = { name : 'bingdian', getName : function() { return function() { return this.name; }; } } console.log(o.getName()()); //Jack var name = 'Jack'; var o = { name : 'bingdian', getName : function() { var self = this; return function() { return self.name; }; } } console.log(o.getName()()); //bingdian
內(nèi)存泄露
function assignHandler() { var el = document.getElementById('demo'); el.onclick = function() { console.log(el.id); } } assignHandler();
以上代碼創(chuàng)建了作為el元素事件處理程序的閉包,而這個(gè)閉包又創(chuàng)建了一個(gè)循環(huán)引用,只要匿名函數(shù)存在,el的引用數(shù)至少為1,因些它所占用的內(nèi)存就永完不會(huì)被回收。
function assignHandler() { var el = document.getElementById('demo'); var id = el.id; el.onclick = function() { console.log(id); } el = null; } assignHandler();
把變量el設(shè)置null能夠解除DOM對(duì)象的引用,確保正?;厥掌湔加脙?nèi)存。
模仿塊級(jí)作用域
任何一對(duì)花括號(hào)({和})中的語句集都屬于一個(gè)塊,在這之中定義的所有變量在代碼塊外都是不可見的,我們稱之為塊級(jí)作用域。
(function(){ //塊級(jí)作用域 })();
閉包的應(yīng)用
保護(hù)函數(shù)內(nèi)的變量安全。如前面的例子,函數(shù)outer中i只有函數(shù)inner才能訪問,而無法通過其他途徑訪問到,因此保護(hù)了i的安全性。
在內(nèi)存中維持一個(gè)變量。如前面的例子,由于閉包,函數(shù)outer中i的一直存在于內(nèi)存中,因此每次執(zhí)行rs(),都會(huì)給i加1。
相關(guān)文章
簡介JavaScript中toUpperCase()方法的使用
這篇文章主要介紹了JavaScript中的toUpperCase()方法的使用,是JS入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06JavaScript中的操作符類型轉(zhuǎn)換示例總結(jié)
最近發(fā)現(xiàn)在前端的面試題目中經(jīng)常出現(xiàn)操作符類型轉(zhuǎn)換的題目,于是這里來進(jìn)行一個(gè)JavaScript中的操作符類型轉(zhuǎn)換示例總結(jié),需要的朋友可以參考下2016-05-05Javascript核心讀書有感之詞法結(jié)構(gòu)
這篇文章主要介紹了Javascript核心讀書有感之詞法結(jié)構(gòu),需要的朋友可以參考下2015-02-02javascript單引號(hào)和雙引號(hào)的區(qū)別和處理
這篇文章主要介紹了javascript單引號(hào)和雙引號(hào)的區(qū)別和處理,希望對(duì)大家有所幫助2014-05-05淺析四種常見的Javascript聲明循環(huán)變量的書寫方式
這篇文章主要介紹了四種常見的聲明循環(huán)變量的書寫方式,對(duì)其進(jìn)行簡單的分析和比較,需要的朋友可以參考下2015-10-10JavaScript中setUTCFullYear()方法的使用簡介
這篇文章主要介紹了JavaScript中setUTCFullYear()方法的使用簡介,是JS入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06