您好,登錄后才能下訂單哦!
這篇文章給大家介紹Javascript中的閉包有什么用,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
var array = []; array.length = 10000000;//(一千萬(wàn)) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date(); console.log(t2-t1); //以下是連續(xù)5次的運(yùn)行時(shí)間 //168+158+170+159+165 = 820(ms)
我們?cè)倏聪旅嬉欢未a, 測(cè)試環(huán)境為 chrome 52.0.2743.116 (64-bit):
var t1 = +new Date(); (function(){//閉包 for(var i=0,length=array.length;i<length;i++){ //array.push(i); } })(); var t2 = +new Date(); console.log(t2-t1); //以下是連續(xù)5次的運(yùn)行時(shí)間: //8+6+8+7+6 = 35(ms)
計(jì)算一下: 820/35 = 23 效率提升大致20倍. 實(shí)際上, 在 Firefox 及 Safari 對(duì) for有做底層優(yōu)化的情況下, 仍然有4~6倍的性能提升. 這是為什么呢?
我們注意到兩段代碼最大的區(qū)別就是, 第二段代碼使用了匿名函數(shù)包裹for循環(huán). 我們將在后面講到, 請(qǐng)耐心閱讀.
作用域
所謂作用域, 指的是, 變量在聲明它們的函數(shù)體以及這個(gè)函數(shù)體嵌套的任意函數(shù)體內(nèi)都是有定義的.
js中只有函數(shù)作用域
眾所周知, JS中并沒(méi)有塊作用域, 只有函數(shù)作用域. 如下:
for(var i=0;i<10;i++){ ; } console.log(i);//10 function f(){ var a = 123; } f(); console.log(a);//a is not defined
因此 js 中只有一種局部作用域, 即函數(shù)作用域.
使用 var 聲明變量
通常我們知道, js 作為一種弱類(lèi)型語(yǔ)言, 聲明一個(gè)變量只需要var保留字, 如果在函數(shù)中不使用 var 聲明變量, 該變量將提升為全局變量, 進(jìn)而脫離函數(shù)作用域, 如下:
function f(){ b = 123; } f(); console.log(b);//123
此時(shí)相對(duì)于前面使用var聲明的 a 變量, b 變量被提升為全局變量, 在函數(shù)作用域外依然可以訪(fǎng)問(wèn).
既然在函數(shù)作用域內(nèi)不使用 var 聲明變量, 會(huì)將變量提升為全局變量, 那么在全局下, 不使用var, 會(huì)怎么樣呢?
//全局下不使用var聲明,該變量依然是全局變量 c = "hello scope"; console.log(c);//hello scope console.log(window.c);//hello scope //查看c變量的屬性 console.log(Object.getOwnPropertyDescriptor(window, 'c'));//Object {value: "hello scope", writable: true, enumerable: true, configurable: true} ,此時(shí)c變量可賦值,可列舉,可配置 //試著刪除c變量 delete c;//true 表示c變量被成功刪除 console.log(c);//c is not defined console.log(window.c);//undefined //使用var聲明后再刪除d變量 var d = 1; console.log(Object.getOwnPropertyDescriptor(window, 'd'));//Object {value: 1, writable: true, enumerable: true, configurable: false} ,此時(shí)d變量可賦值,可列舉,但不可配置 delete d;//false 表示d變量刪除失敗 console.log(d);//1 console.log(window.d);//1
綜上, 有如下規(guī)律:
不使用var保留字聲明變量, 變量提升為全局變量, 而不論變量處于哪種作用域;
如果不使用var聲明, 該變量便可配置, 即可被 delete 保留字刪除, 刪除后該變量便不可訪(fǎng)問(wèn); 如果使用var聲明, 該變量便不可配置, 即不能被 delete 保留字刪除;
只要是全局變量都可以直接訪(fǎng)問(wèn), 也可使用 “window.變量名” 來(lái)訪(fǎng)問(wèn), 不管該變量是不是通過(guò)var來(lái)聲明的;
JS中的作用域鏈
函數(shù)對(duì)象和其它對(duì)象一樣,擁有可以通過(guò)代碼訪(fǎng)問(wèn)的屬性和一系列僅供JavaScript引擎訪(fǎng)問(wèn)的內(nèi)部屬性。其中一個(gè)內(nèi)部屬性是[[Scope]],由ECMA-262標(biāo)準(zhǔn)第三版定義,該內(nèi)部屬性包含了函數(shù)被創(chuàng)建的作用域中對(duì)象的集合,這個(gè)集合被稱(chēng)為函數(shù)的作用域鏈,它決定了哪些數(shù)據(jù)能被函數(shù)訪(fǎng)問(wèn)。
我們先看一個(gè)栗子:
var e = "hello"; function f(){ e = "scope chain"; var g = = "good"; }
以上作用域鏈的圖如下所示:
函數(shù)執(zhí)行時(shí), 在函數(shù) f 內(nèi)部會(huì)生成一個(gè) active object 和 scope chain. JavaScript引擎內(nèi)部對(duì)象會(huì)放入 active object中, 外部的 e 變量處于scope chain的第二層, index=1, 而內(nèi)部的g變量處于scope chain的頂層, index=0, 因此訪(fǎng)問(wèn)g變量總比訪(fǎng)問(wèn)e變量來(lái)的快些.
閉包
聊到作用域, 就不得不說(shuō)閉包, 那么, 什么是閉包?
“官方”的解釋是:閉包是一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù)),因而這些變量也是該表達(dá)式的一部分。
這是什么意思呢, 簡(jiǎn)單來(lái)說(shuō)就是:
函數(shù)執(zhí)行時(shí)返回內(nèi)部私有函數(shù), 或者通過(guò)其他方式將內(nèi)部私有函數(shù)保留在外(比如說(shuō)通過(guò)將其內(nèi)部私有函數(shù)的引用賦值外部變量), 從而阻止該函數(shù)內(nèi)部作用域等被執(zhí)行引擎回收.
在函數(shù)外部通過(guò)訪(fǎng)問(wèn)暴露在外的函數(shù)內(nèi)部私有函數(shù), 從而具有訪(fǎng)問(wèn)函數(shù)內(nèi)部私有作用域的效果, 就是閉包.
ES6之前, 通常我們實(shí)現(xiàn)的模塊就是利用了閉包. 閉包依賴(lài)的結(jié)構(gòu)有個(gè)鮮明的特點(diǎn), 即: 一個(gè)函數(shù)在詞法作用域之外執(zhí)行. 如下, f2是閉包的關(guān)鍵, 它的詞法作用域便是函數(shù)f的內(nèi)部私有作用域, 且它在f的作用域外部執(zhí)行.
var h = 1; function f(){ var i = 2; return function f2(){ var j = 3 + i + h; console.log(j); } } var ff = f(); ff();//6
由于定義時(shí) f2 處于 f 的內(nèi)部, 因此 f2 內(nèi)可以訪(fǎng)問(wèn)到 f 的內(nèi)部私有作用域, 這樣通過(guò)返回 f2 就能保證在 f 函數(shù)外部也能訪(fǎng)問(wèn)到 i 變量.
當(dāng)f2執(zhí)行時(shí), 變量 j 處于scope chain的 index0的位置上, 變量 i 和變量 h 分別處于 scope chain 的 index1 index2 的位置上. 因此 j 的賦值過(guò)程其實(shí)就是沿著 scope chain 第二層 第三層 依次找到 i 和 h 的值, 然后將它們和3一起求和, 最終賦值給 j .
瀏覽器沿著 scope chain 尋找變量總是需要耗費(fèi)CPU時(shí)間, 越是 scope chain 的 外層(或者離f2越遠(yuǎn)的變量), 瀏覽器查找起來(lái)越是需要時(shí)間, 因?yàn)?scope chain 需要?dú)v經(jīng)更多次遍歷. 因此全局變量(window)總是需要最多的訪(fǎng)問(wèn)時(shí)間.
閉包內(nèi)的微觀(guān)世界
如果要更加深入的了解閉包以及函數(shù) f 和嵌套函數(shù) f2 的關(guān)系,我們需要引入另外幾個(gè)概念:函數(shù)的執(zhí)行環(huán)境(excution context)、活動(dòng)對(duì)象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數(shù)a從定義到執(zhí)行的過(guò)程為例闡述這幾個(gè)概念。
當(dāng)定義函數(shù) f 的時(shí)候, js解釋器會(huì)將函數(shù)a的作用域鏈(scope chain)設(shè)置為定義 f 時(shí) a 所在的”環(huán)境”, 如果 f 是一個(gè)全局函數(shù),則scope chain中只有window對(duì)象。
當(dāng)執(zhí)行函數(shù) f 的時(shí)候, f 會(huì)進(jìn)入相應(yīng)的執(zhí)行環(huán)境(excution context).
在創(chuàng)建執(zhí)行環(huán)境的過(guò)程中, 首先會(huì)為 f 添加一個(gè)scope屬性, 即a的作用域, 其值就為第1步中的scope chain. 即a.scope=f 的作用域鏈.
然后執(zhí)行環(huán)境會(huì)創(chuàng)建一個(gè)活動(dòng)對(duì)象(call object). 活動(dòng)對(duì)象也是一個(gè)擁有屬性的對(duì)象, 但它不具有原型而且不能通過(guò)JavaScript代碼直接訪(fǎng)問(wèn). 創(chuàng)建完活動(dòng)對(duì)象后, 把活動(dòng)對(duì)象添加到 f 的作用域鏈的最頂端. 此時(shí)a的作用域鏈包含了兩個(gè)對(duì)象: f 的活動(dòng)對(duì)象和window對(duì)象.
下一步是在活動(dòng)對(duì)象上添加一個(gè)arguments屬性, 它保存著調(diào)用函數(shù) f 時(shí)所傳遞的參數(shù).
最后把所有函數(shù) f 的形參和內(nèi)部的函數(shù) f2 的引用也添加到 f 的活動(dòng)對(duì)象上. 在這一步中, 完成了函數(shù) f2 的定義, 因此如同第3步, 函數(shù) f2 的作用域鏈被設(shè)置為 f2 所被定義的環(huán)境, 即 f 的作用域.
到此, 整個(gè)函數(shù) f 從定義到執(zhí)行的步驟就完成了. 此時(shí) f 返回函數(shù) f2 的引用給 ff, 又函數(shù) f2 的作用域鏈包含了對(duì)函數(shù) f 的活動(dòng)對(duì)象的引用, 也就是說(shuō) f2 可以訪(fǎng)問(wèn)到 f 中定義的所有變量和函數(shù). 函數(shù) f2 被 ff 引用, 函數(shù) f2又依賴(lài)函數(shù) f , 因此函數(shù) f 在返回后不會(huì)被GC回收.
當(dāng)函數(shù) f2 執(zhí)行的時(shí)候亦會(huì)像以上步驟一樣. 因此, 執(zhí)行時(shí) f2 的作用域鏈包含了3個(gè)對(duì)象: f2 的活動(dòng)對(duì)象、f 的活動(dòng)對(duì)象和window對(duì)象, 如下圖所示:
如圖所示, 當(dāng)在函數(shù) f2 中訪(fǎng)問(wèn)一個(gè)變量的時(shí)候, 搜索順序是:
先搜索自身的活動(dòng)對(duì)象, 如果存在則返回, 如果不存在將繼續(xù)搜索函數(shù) f 的活動(dòng)對(duì)象, 依次查找, 直到找到為止.
如果函數(shù) f2 存在prototype原型對(duì)象, 則在查找完自身的活動(dòng)對(duì)象后先查找自身的原型對(duì)象, 再繼續(xù)查找. 這就是Javascript中的變量查找機(jī)制.
如果整個(gè)作用域鏈上都無(wú)法找到, 則返回undefined.
小結(jié), 本段中提到了兩個(gè)重要的詞語(yǔ): 函數(shù)的定義與執(zhí)行. 文中提到函數(shù)的作用域是在定義函數(shù)時(shí)候就已經(jīng)確定, 而不是在執(zhí)行的時(shí)候確定(參看步驟1和3).用一段代碼來(lái)說(shuō)明這個(gè)問(wèn)題:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
這段代碼中變量h指向了f中的那個(gè)匿名函數(shù)(由g返回).
假設(shè)函數(shù)h的作用域是在執(zhí)行alert(h())
確定的, 那么此時(shí)h的作用域鏈?zhǔn)? h的活動(dòng)對(duì)象->alert的活動(dòng)對(duì)象->window對(duì)象.
假設(shè)函數(shù)h的作用域是在定義時(shí)確定的, 就是說(shuō)h指向的那個(gè)匿名函數(shù)在定義的時(shí)候就已經(jīng)確定了作用域. 那么在執(zhí)行的時(shí)候, h的作用域鏈為: h的活動(dòng)對(duì)象->f的活動(dòng)對(duì)象->window對(duì)象.
如果第一種假設(shè)成立, 那輸出值就是undefined; 如果第二種假設(shè)成立, 輸出值則為1。
運(yùn)行結(jié)果證明了第2個(gè)假設(shè)是正確的,說(shuō)明函數(shù)的作用域確實(shí)是在定義這個(gè)函數(shù)的時(shí)候就已經(jīng)確定了.
閉包有可能導(dǎo)致IE瀏覽器內(nèi)存泄漏
先看一個(gè)栗子:
function f(){ var div = document.createElement("div"); div.onclick = function(){ return false; } }
上述div的click事件就是一個(gè)閉包, 由于該閉包的存在使得 f 函數(shù)內(nèi)部的 div 變量對(duì)DOM元素的引用將一直存在.
而早期IE瀏覽器( IE9之前 ) js 對(duì)象和 DOM 對(duì)象使用不同的垃圾收集方法, DOM對(duì)象使用計(jì)數(shù)垃圾回收機(jī)制, 只要匿名函數(shù)( 比如說(shuō)onclick事件 )存在, DOM對(duì)象的引用便至少為1,因此它所占用的內(nèi)存就永遠(yuǎn)不會(huì)被銷(xiāo)毀.
有趣的是,不同的IE版本將導(dǎo)致不同的現(xiàn)象:
如果是IE 6, 內(nèi)存泄漏,直到關(guān)閉IE進(jìn)程為止;
如果是IE 7,內(nèi)存泄漏, 直到離開(kāi)當(dāng)前頁(yè)面為止;
如果是IE 8, GC回收器回收他們的內(nèi)存,無(wú)論當(dāng)前是不是compatibility模式.
總結(jié)一下, 閉包的優(yōu)點(diǎn): 共享函數(shù)作用域, 便于開(kāi)放一些接口或變量供外部使用;
注意事項(xiàng): 由于閉包可能會(huì)使得函數(shù)中變量被長(zhǎng)期保存在內(nèi)存中, 從而大量消耗內(nèi)存, 影響頁(yè)面性能, 因此不能濫用, 并且在IE瀏覽中可能導(dǎo)致內(nèi)存泄露. 解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除.
for循環(huán)問(wèn)題分析
我們?cè)賮?lái)看看開(kāi)篇的for循環(huán)問(wèn)題, 增加匿名函數(shù)后, for循環(huán)內(nèi)部的變量便處于匿名函數(shù)的局部作用域下, 此時(shí)訪(fǎng)問(wèn) length 屬性, 或者訪(fǎng)問(wèn) i 屬性, 都只需要在匿名函數(shù)作用域內(nèi)查找即可, 因此查詢(xún)效率大大提升(測(cè)試數(shù)據(jù)發(fā)現(xiàn)提升有兩百多倍).
使用匿名函數(shù)后, 不止是作用域查詢(xún)更快, 作用域內(nèi)的變量還與外部隔離, 避免了像 i , length 這樣的變量對(duì)后續(xù)代碼產(chǎn)生影響. 可謂一舉兩得.
踩個(gè)作用域的坑
下面我們來(lái)踩一個(gè)作用域經(jīng)典的坑.
var div = document.getElementsByTagName("div"); for(var i=0,len=div.length;i<len;i++){ div[i].onclick = function(){ console.log(i); } }
上述代碼的本意是每次點(diǎn)擊div, 打印div的索引, 實(shí)際上打印的卻是 len 的值. 我們來(lái)分析下原因.
點(diǎn)擊div時(shí), 將會(huì)執(zhí)行 console.log(i)
語(yǔ)句, 顯然 i 變量不在 click 事件的局部作用域內(nèi), 瀏覽器將沿著 scope chain 尋找 i 變量, 在 index1
的地方, 即 for循環(huán)開(kāi)始的地方, 此處定義了一個(gè) i 變量, 又 js 沒(méi)有塊作用域, 故 i 變量并不會(huì)在 for循環(huán)塊執(zhí)行完成后被銷(xiāo)毀,又 i的最后一次自加使得 i = len
, 于是瀏覽器在scope chain index=1
索引的地方停下來(lái)了, 返回了i的值, 即len的值.
為了解決這個(gè)問(wèn)題, 我們將根據(jù)癥結(jié), 對(duì)癥下藥, 從作用域入手, 改變click事件的局部作用域, 如下:
var div = document.getElementsByTagName("div"); for(var i=0,len=div.length;i<len;i++){ (function(n){ div[n].onclick = function(){ console.log(n); } })(i); }
由于 click 事件被閉包包裹, 并且閉包自執(zhí)行, 因此閉包內(nèi) n 變量的值每次都不一樣, 點(diǎn)擊div時(shí), 瀏覽器將沿著 scope chain 尋找 n 變量, 最終會(huì)找到閉包內(nèi)的 n 變量, 并且打印出div 的索引.
this作用域
前面我們學(xué)習(xí)了作用域鏈, 閉包等基礎(chǔ)知識(shí), 下面我們來(lái)聊聊神秘莫測(cè)的this作用域.
熟悉OOP的開(kāi)發(fā)人員都知道, this是對(duì)象實(shí)例的引用, 始終指向?qū)ο髮?shí)例. 然而 js 的世界里, this隨著它的執(zhí)行環(huán)境改變而改變, 并且它總是指向它所在方法的對(duì)象. 如下,
function f(){ alert(this); } var o = {}; o.func = f; f();//[object Window] o.func();//[object Object] console.log(f===window.f);//true
當(dāng)f單獨(dú)執(zhí)行時(shí), 其內(nèi)部this指向window對(duì)象, 但是當(dāng)f成為o對(duì)象的屬性func時(shí), this指向的是o對(duì)象, 又f === window.f
, 故它們實(shí)際上指向的都是this所在方法的對(duì)象.
下面我們來(lái)應(yīng)用下
Array.prototype.slice.call([1,2,3],1);//[2,3],正確用法 Array.prototype.slice([1,2,3],1);//[], 錯(cuò)誤用法,此時(shí)slice內(nèi)部this仍然指向Array.prototype var slice = Array.prototype.slice; slice([1,2,3],1);//Uncaught TypeError: Array.prototype.slice called on null or undefined //此時(shí)slice內(nèi)部this指向的是window對(duì)象,離開(kāi)了原來(lái)的Array.prototype對(duì)象作用域,故報(bào)錯(cuò)~~
總結(jié)下, this的使用只需要注意一點(diǎn):
this 總是指向它所在方法的對(duì)象.
with語(yǔ)句
聊到作用域鏈就不得不說(shuō)with語(yǔ)句了, with語(yǔ)句可以用來(lái)臨時(shí)改變作用域, 將語(yǔ)句中的對(duì)象添加到作用域的頂部.
語(yǔ)法: with (expression){statement}
例如:
var k = {name:"daicy"}; with(k){ console.log(name);//daicy } console.log(name);//undefined
with 語(yǔ)句用于對(duì)象 k, 作用域第一層為 k 對(duì)象內(nèi)部作用域, 故能直接打印出 name 的值, 在with之外的語(yǔ)句不受此影響.
再看一個(gè)栗子:
var l = [1,2,3]; with(l) { console.log(map(function(i){ return i*i; }));//[1,4,9] }
在這個(gè)例子中,with 語(yǔ)句用于數(shù)組,所以在調(diào)用 map()
方法時(shí),解釋程序?qū)z查該方法是否是本地函數(shù)。如果不是,它將檢查偽對(duì)象 l,看它是否為該對(duì)象的方法, 又map是Array對(duì)象的方法, 數(shù)組l繼承了該方法, 故能正確執(zhí)行.
注意: with語(yǔ)句容易引起歧義, 由于需要強(qiáng)制改變作用域鏈, 它將帶來(lái)更多的cpu消耗, 建議慎用 with 語(yǔ)句.
關(guān)于Javascript中的閉包有什么用就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。