整合營(yíng)銷(xiāo)服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          JavaScript: 零基礎(chǔ)輕松學(xué)閉包,實(shí)戰(zhàn)

          JavaScript: 零基礎(chǔ)輕松學(xué)閉包,實(shí)戰(zhàn)

          文面向初學(xué)者,大神輕噴。

          好了,開(kāi)始吧。

          上一節(jié) JavaScript: 零基礎(chǔ)輕松學(xué)閉包(1)中,我們對(duì)閉包的原理進(jìn)行了講解,這一節(jié)會(huì)說(shuō)很多實(shí)戰(zhàn)性的東西了,可能會(huì)有點(diǎn)難度,你準(zhǔn)備好了嗎?

          1. 如何將私有數(shù)據(jù)暴露出去

          還記得在上一節(jié)中,有這樣一個(gè)例子么?

          var test=function(){
           var i=10;
          }
          function test2(){
           alert(i);
          }
          test2();
          

          函數(shù) test 和 test2 各自形成一個(gè)閉包,兩個(gè)閉包之間無(wú)法訪問(wèn)對(duì)方的私有數(shù)據(jù)。比如,在 test 中定義的變量,在 test2 里面是無(wú)法直接訪問(wèn)到的。

          那么問(wèn)題來(lái)了, 當(dāng)然,這邊和挖掘機(jī)沒(méi)關(guān)系。這里的問(wèn)題是,有沒(méi)有什么辦法讓 test2 可以訪問(wèn)到其他閉包中的私有變量呢?

          辦法當(dāng)然是有的,最直接的想法就是,大不了我定義一個(gè)全局變量,在 test 中將私有數(shù)據(jù)賦給全局變量,然后在 test2 里面就能訪問(wèn)到了。

          是的,因?yàn)閮蓚€(gè)函數(shù)共同享有一個(gè)全局作用域,所以這個(gè)辦法確實(shí)可行。我在很多項(xiàng)目里也的確看到很多人就是這么做的。

          那么,有沒(méi)有一種更好的方法呢?要知道,全局作用域是一個(gè)比較敏感的地方,一不小心就會(huì)出現(xiàn)變量名重復(fù)的問(wèn)題。順便說(shuō)一句,在全局作用域中,盡量不要使用諸如 temp , a , b , c 這一類(lèi)的大眾化變量。

          于是,這就牽扯到返回值的相關(guān)知識(shí)了,你在C語(yǔ)言的教材中肯定見(jiàn)慣了類(lèi)似于這樣的代碼

          int sum(int a,int b)
          {
           return a + b;
          }
          int all=sum(3,5);
          

          這是一個(gè)簡(jiǎn)單的求和函數(shù),很多人慢慢地養(yǎng)成了這樣一個(gè)觀念,就是函數(shù)的返回值就是一個(gè)字面值,要么是數(shù)字類(lèi)型,要么是布爾類(lèi)型,或者是字符串。

          在很多強(qiáng)類(lèi)型的語(yǔ)言,諸如 Java,C,C++, 確實(shí)如此。但是 return 在 JavaScript 中卻大有來(lái)頭。

          在上一節(jié)已經(jīng)說(shuō)明了,js 的函數(shù)也是一種數(shù)據(jù)類(lèi)型,你可以把函數(shù)看成是和int , float , double 一樣的東西。

          那么,既然int可以當(dāng)做函數(shù)的參數(shù)或者返回值,函數(shù)當(dāng)然也可以!

          請(qǐng)看下面兩句話:

          在js中

          • 如果函數(shù)被當(dāng)做參數(shù)傳進(jìn)去了,它就是所謂的回調(diào)函數(shù)。
          • 如果函數(shù)被當(dāng)做返回值return出去了,它就是把一個(gè)閉包return出去了。

          這一章不講回調(diào)函數(shù),如果你不清楚啥叫回調(diào)函數(shù),可以去看看這個(gè)小例子:

          (淺談js回調(diào)函數(shù))

          還是上面的那個(gè)例子,我們希望在 test2 中可以訪問(wèn)到 test 里面的變量,可以這樣做:

          var test=function(){
           var i=10;
           /* 定義一個(gè)函數(shù)將變量i暴露出去 */
           var get=function(){
           return i ;
           }
           return get; //將獲得i的函數(shù)暴露出去
          }
          function test2(){
           
           var fn=test();//接收test暴露出來(lái)的函數(shù)
           alert(fn()); //獲得test中的私有數(shù)據(jù)
          }
          test2();
          

          test 函數(shù)中的 get 方法是一個(gè)內(nèi)部函數(shù),它自己也形成了一個(gè)閉包, test 是他的父級(jí)作用域,因此它可以獲取i的值。

          i 進(jìn)入 get 方法的閉包,被包了起來(lái),然后最終被返回了出去。

          而對(duì)于 test2 來(lái)說(shuō),是可以訪問(wèn)到 test函數(shù)的,因此可以調(diào)用并執(zhí)行 test 函數(shù),從而獲取其返回值。

          你可能會(huì)說(shuō),我直接在test中把i給return出去就好了嘛,干嘛這么麻煩。

          是的,言之有道理。

          可是,如果我要訪問(wèn) test 中多個(gè)私有數(shù)據(jù)咋辦捏?

          這下你可明白了吧!

          現(xiàn)在,我們給出關(guān)于閉包的第二個(gè)注解:

          (第一個(gè)注解在上一節(jié))

          從應(yīng)用的角度來(lái)看,閉包可以將函數(shù)或者對(duì)象的私有數(shù)據(jù)暴露出去,而不影響全局作用域。

          通過(guò)這張圖,是不是好理解一些了呢?我們這一節(jié)單說(shuō)函數(shù)里的私有數(shù)據(jù)。

          2. 將私有數(shù)據(jù)包裝成json對(duì)象

          剛才的例子說(shuō)明,在js中,return出去的可以是基本數(shù)據(jù)類(lèi)型,也可以是函數(shù)類(lèi)型。

          其實(shí),JavaScript是一種基于對(duì)象的語(yǔ)言,也有對(duì)象的概念,所以,我們可以把你需要的東西包裹成一個(gè)對(duì)象返回出去!

          上代碼:

          var test=function(){
           var apple='蘋(píng)果';
           var pear='梨子';
           /* 定義一個(gè)函數(shù)將水果暴露出去 */
           var getFruit={
           apple : apple ,
           pear : pear
           }
           
           return getFruit; //將獲得i的函數(shù)暴露出去
          }
          function test2(){
           
           var getFruit=test();//接收test暴露出來(lái)的函數(shù)
           
           console.log(getFruit);
          }
          test2();
          

          像這樣用 { } 括起來(lái)的東西就是一個(gè)js對(duì)象,也就是所謂json。你可能經(jīng)常會(huì)聽(tīng)到j(luò)son這個(gè)詞,覺(jué)得還挺高大上的。其實(shí)它就是一個(gè)用 { } 包起來(lái)的數(shù)據(jù)而已。

          里面是鍵值對(duì)的形式,非常類(lèi)似于Java里面的HashMap。

          在這個(gè)例子中,我們可以直接把需要暴露的私有數(shù)據(jù)用一個(gè) { } 包起來(lái),構(gòu)成一個(gè)json對(duì)象return出去就可以啦。

          因?yàn)槭?js 對(duì)象,alert 不能看到里面的具體內(nèi)容,所以我們使用 console.log() ,結(jié)果如下:

          展開(kāi)后:

          Paste_Image.png

          這樣是不是也可以了?多出來(lái)的 proto 是原型鏈,以后會(huì)講到。

          3. 我們來(lái)做一個(gè)紫金葫蘆

          大家都還記得西游記里孫悟空用遮天的把戲騙來(lái)的紫金葫蘆嗎,只要你拿著這個(gè)葫蘆,叫一聲別人的名字,如果答應(yīng)了,別人就會(huì)被吸進(jìn)去。

          OK,這個(gè)紫金葫蘆里面不正如一個(gè)閉包嗎?

          對(duì)不對(duì)嘛,所以,我們用閉包的知識(shí)來(lái)做一個(gè)好玩的東西吧。

          <body>
           <div id='box' style='width:50px;height:50px;background:#333;color:#fff;text-align:center;line-height:50px'>小妖</div>
          </body>
          

          紫金葫蘆里面的源碼大概是這樣的:

          var 紫金葫蘆=function(id){
           var domElement=document.getElementById(id);
           
           var returnObject={
           domElement : domElement ,
           backgroundColor : function(color){
           domElement.style.backgroundColor=color;
           },
           click : function(fn){
           domElement.onclick=fn;
           }
           };
           
           return returnObject;
          }
          

          注:我純粹是為了看起來(lái)方便而采用中文定義變量,在實(shí)際開(kāi)發(fā)中,千萬(wàn)不要使用中文變量。

          我們?cè)诜祷爻鋈サ膶?duì)象上加了三個(gè)東西:

          1.domElement

          你傳進(jìn)來(lái)一個(gè)id,我就用 document.getElementById 來(lái)包一下,得到一個(gè)dom元素,最終要操作的也就是這個(gè)dom元素。也就是說(shuō):

          var box1=紫金葫蘆('box').domElement;
          var box2=document.getElementById('box');
          alert(box1===box2);
          

          他們是一個(gè)東西,一樣的。

          紫金葫蘆('box');
          

          這行代碼一旦執(zhí)行,紫金葫蘆就會(huì)返回 returnObject 對(duì)象,也就是說(shuō)。我們喊一聲 “box”,那個(gè)id為box的小妖一答應(yīng),就被裝進(jìn)來(lái)了,然后我們可以對(duì)它為所欲為!

          比如,給它換一個(gè)背景色:

          2.backgroundColor 給元素添加背景色的方法

          var box=紫金葫蘆('box');
          box.backgroundColor('red');
          

          3.click 給元素添加點(diǎn)擊事件,需要傳入一個(gè)回調(diào)函數(shù)

          var box=紫金葫蘆('box');
          box.click(function(){
           alert('就沒(méi)人吐槽這個(gè)無(wú)聊的作者么,小妖也有尊嚴(yán)的好么,啊喂!!');
          });
          

          結(jié)果:

          也許你已經(jīng)發(fā)現(xiàn)了,這些方法是不是和jQuery有點(diǎn)類(lèi)似呢?

          閉包有關(guān)的兩個(gè)概念:

          (1)變量的作用域 不帶有關(guān)鍵字var的變量會(huì)成為全局變量; 在函數(shù)中使用關(guān)鍵字var聲明的變量是局部變量。 局部變量只有在函數(shù)內(nèi)部才能訪問(wèn)到,在函數(shù)外面是訪問(wèn)不到的。但在函數(shù)內(nèi)部可以通過(guò)作用域鏈一直向上搜索直到全局對(duì)象,也就是說(shuō),函數(shù)內(nèi)部可以訪問(wèn)函數(shù)外部的變量。

          (2)變量的生存周期 對(duì)于全局變量,其生存周期是永久的,除非主動(dòng)銷(xiāo)毀這個(gè)全局變量; 而對(duì)于在函數(shù)內(nèi)用關(guān)鍵字var聲明的局部變量,當(dāng)退出函數(shù)時(shí),這些局部變量會(huì)隨著函數(shù)調(diào)用結(jié)束而被銷(xiāo)毀。

          例外情況:閉包

          從閉包的一個(gè)經(jīng)典應(yīng)用談起

          問(wèn)題:無(wú)論單擊哪個(gè)div,都會(huì)彈出5。 原因:onclick事件是異步觸發(fā)的,當(dāng)事件被觸發(fā)時(shí),for循環(huán)早已結(jié)束,此時(shí)變量i的值早已經(jīng)是5。 解決:在閉包的幫助下,把每次循環(huán)的i值都封閉起來(lái)。當(dāng)事件函數(shù)順著作用域鏈從內(nèi)到外查找變量i時(shí),會(huì)先找到被封閉在閉包環(huán)境的i,單擊div時(shí),會(huì)分別輸出0,1,2,3,4。

          類(lèi)似實(shí)例:閉包直接賦給數(shù)組

          結(jié)果:result的每個(gè)元素都返回10。 說(shuō)明:閉包的作用域鏈有明顯的副作用——閉包總是獲得外部函數(shù)變量的最終值。上面代碼中,外部函數(shù)產(chǎn)生一個(gè)函數(shù)數(shù)組result并返回。函數(shù)數(shù)組中的每個(gè)元素都是一個(gè)函數(shù),每個(gè)函數(shù)都返回 i變量。似乎每個(gè)函數(shù)應(yīng)該返回每次循環(huán)的i值,即依次返回0到9,但事實(shí)是,每個(gè)函數(shù)的返回結(jié)果都是10。這是因?yàn)槊總€(gè)內(nèi)部函數(shù)返回的是變量i,而不是i在某個(gè)時(shí)刻的特定值,而i的作用域是整個(gè)外部函數(shù),當(dāng)外部函數(shù)執(zhí)行完成后,i的值是10。 解決:在每個(gè)內(nèi)部函數(shù)的內(nèi)部,再產(chǎn)生一個(gè)匿名函數(shù)并返回。

          結(jié)果:result依次返回0到9。 說(shuō)明:(i)使得該層匿名函數(shù)立即執(zhí)行。 ###3、閉包

          有時(shí)候需要得到函數(shù)內(nèi)的局部變量。如何從外部讀取局部變量?那就是在函數(shù)的內(nèi)部,再定義一個(gè)函數(shù)。 閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中變量的函數(shù),創(chuàng)建閉包的最常見(jiàn)的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù),通過(guò)另一個(gè)函數(shù)訪問(wèn)這個(gè)函數(shù)的局部變量,利用閉包可以突破作用鏈域,將函數(shù)內(nèi)部的變量和方法傳遞到外部。 ① 閉包的原理

          • 后臺(tái)執(zhí)行環(huán)境中,閉包的作用域鏈包含著自己的作用域、函數(shù)的作用域和全局作用域。
          • 通常,函數(shù)的作用域和變量會(huì)在函數(shù)執(zhí)行結(jié)束后銷(xiāo)毀。
          • 但是,當(dāng)函數(shù)返回一個(gè)閉包時(shí),這個(gè)函數(shù)的作用域?qū)?huì)一直在內(nèi)存中保存到閉包不存在為止。 ② 閉包的特性
          • 函數(shù)內(nèi)再嵌套函數(shù)。
          • 內(nèi)部函數(shù)可以引用外層的參數(shù)和變量。
          • 參數(shù)和變量不會(huì)被垃圾回收機(jī)制回收。 ③ 閉包的用途
          • 讀取函數(shù)內(nèi)部的變量。

          在上面的代碼中,函數(shù)f2就被包括在函數(shù)f1內(nèi)部,這時(shí)f1內(nèi)部的所有局部變量,對(duì)f2都是可見(jiàn)的。但是反過(guò)來(lái)就不行,f2內(nèi)部的局部變量,對(duì)f1就是不可見(jiàn)的。既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,就可以在f1外部讀取它的內(nèi)部變量了。

          閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在Javascript語(yǔ)言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取局部變量,因此可以把閉包簡(jiǎn)單理解成"定義在一個(gè)函數(shù)內(nèi)部的函數(shù)"。所以,在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)的一座橋梁。

          result實(shí)際上就是閉包f2函數(shù)。它一共運(yùn)行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數(shù)f1中的局部變量n一直保存在內(nèi)存中,并沒(méi)有在f1調(diào)用后被自動(dòng)清除。原因就在于f1是f2的父函數(shù),而f2被賦給了一個(gè)全局變量,這導(dǎo)致f2始終在內(nèi)存中,而f2的存在依賴(lài)于f1,因此f1也始終在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后,被垃圾回收機(jī)制回收。 (2)讓函數(shù)內(nèi)部的變量的值始終保持在內(nèi)存中(延長(zhǎng)局部變量的壽命)

          由于閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會(huì)造成網(wǎng)頁(yè)的性能問(wèn)題,在IE中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。也就是說(shuō),閉包會(huì)引用外部函數(shù)作用域,會(huì)占用更多的內(nèi)存,過(guò)度使用閉包,會(huì)導(dǎo)致性能問(wèn)題。所以,僅當(dāng)必要時(shí)才使用閉包。對(duì)產(chǎn)生閉包的函數(shù),使用后應(yīng)該解除引用。 (3)自執(zhí)行函數(shù)+閉包減少全局變量污染(封裝私有變量)

          var person=(function() {
           var_name="Alice";
           var _id=16;
           return {
           getUserInfo: function() {
           return _name + ": " + _id;
           }
           }
          })();//幫助突破技術(shù)瓶頸,提升思維能力
          

          使用下劃線來(lái)約定私有變量_name和_age,它們被封裝在閉包產(chǎn)生的作用域中,外部是訪問(wèn)不到這兩個(gè)變量的,這就避免了對(duì)全局的命令污染。 ④ 閉包的缺點(diǎn): (1) 需要維護(hù)額外的作用域。 (2) 過(guò)渡使用閉包會(huì)占用大量?jī)?nèi)存。 4、this對(duì)象 在閉包內(nèi)使用this對(duì)象將產(chǎn)生一些復(fù)雜的行為。this對(duì)象的值基于函數(shù)所在的執(zhí)行環(huán)境在運(yùn)行時(shí)決定:在全局函數(shù)中使用時(shí),this等于window(非嚴(yán)格模式)或undefined(嚴(yán)格模式);而當(dāng)作為對(duì)象的方法調(diào)用時(shí),this等于這個(gè)對(duì)象。

          每個(gè)函數(shù)一旦被調(diào)用,它將自動(dòng)獲得this和arguments兩個(gè)變量。一個(gè)內(nèi)部函數(shù)是不能直接從外部函數(shù)訪問(wèn)到這兩個(gè)變量的。可以通過(guò)將this對(duì)象存儲(chǔ)在另一個(gè)變量中來(lái)解決這個(gè)問(wèn)題。把外部作用域中的this對(duì)象保存在一個(gè)閉包能夠訪問(wèn)到的變量里,就可以讓閉包訪問(wèn)該對(duì)象了。

          var name="The window";
          var object={
           name: "My object",
           getName: function() {
           var that=this;
           return function() {
           return that.name;
           };
           }
          };
          alert(object.getName()());//輸出:"My object"
          

          要讓閉包訪問(wèn)外部函數(shù)的this和arguments對(duì)象,可以通過(guò)將它們的引用存儲(chǔ)在另一個(gè)變量中來(lái)完成。 5、內(nèi)存泄漏 使用閉包的時(shí)候很容易造成循環(huán)引用,若閉包的作用域包含著一些DOM節(jié)點(diǎn),這時(shí)候就有可能造成內(nèi)存泄漏,但其實(shí),這本身不是閉包的問(wèn)題,而是由于:BOM和DOM中的對(duì)象是使用C++以COM對(duì)象的方式實(shí)現(xiàn)的,而COM對(duì)象的垃圾收集機(jī)制采用的是引用計(jì)數(shù)策略,在基于引用計(jì)數(shù)策略的垃圾回收機(jī)制中,若兩個(gè)對(duì)象之間形成了循環(huán)引用,則這兩個(gè)對(duì)象都無(wú)法被回收。

          匿名函數(shù)保存了一個(gè)對(duì)element的引用,只要匿名函數(shù)存在,element的引用數(shù)至少為1,它所占用的內(nèi)存就永遠(yuǎn)不會(huì)被回收。

          通過(guò)把element.id的一個(gè)副本保存在變量中,并且在閉包中引用該變量消除了循環(huán)引用,但是僅僅做到這一步還不能解決內(nèi)存泄漏的問(wèn)題,閉包會(huì)引用包含函數(shù)的所有活動(dòng)對(duì)象,包含element,即使閉包不直接引用element,包含函數(shù)的活動(dòng)對(duì)象中也仍然會(huì)保存一個(gè)引用,因此有必要把element元素設(shè)置為null,這樣就能解除對(duì)DOM對(duì)象的引用,確保正常回收其占用的內(nèi)存。 6、模仿塊級(jí)作用域 JavaScript中沒(méi)有直接的塊級(jí)作用域。

          使用閉包可以模仿塊級(jí)作用域——?jiǎng)?chuàng)建并立即調(diào)用一個(gè)函數(shù),這樣既可以執(zhí)行其中的代碼,又不會(huì)在內(nèi)存中留下對(duì)該函數(shù)的引用。結(jié)果是內(nèi)部函數(shù)的所有變量都會(huì)立即被銷(xiāo)毀,除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。 用作塊級(jí)作用域的匿名函數(shù):將函數(shù)聲明包含在一對(duì)圓括號(hào)中,表示它實(shí)際上是一個(gè)函數(shù)表達(dá)式,緊隨其后的另一對(duì)圓括號(hào)會(huì)立即調(diào)用這個(gè)函數(shù)。

          (function() {
           //這里是塊級(jí)作用域;
          }) ();
          

          可以使用匿名函數(shù)表達(dá)式來(lái)模擬塊級(jí)作用域,任何定義在匿名函數(shù)中的變量在匿名函數(shù)執(zhí)行完之后都將被銷(xiāo)毀,在匿名函數(shù)外訪問(wèn)這些變量將會(huì)產(chǎn)生錯(cuò)誤。

          喜歡的小伙伴,轉(zhuǎn)發(fā)+點(diǎn)個(gè)關(guān)注吧!

          包是什么

          閉包是指那些能夠訪問(wèn)自由變量的函數(shù)。

          《JavaScript高級(jí)程序設(shè)計(jì)第三版》:閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的常見(jiàn)方式,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)。

          《你不知道的JavaScript(上卷)》:當(dāng)函數(shù)可以記住并訪問(wèn)所在的詞法作用域時(shí),就產(chǎn)生了閉包,即使函數(shù)是在當(dāng)前詞法作用域之外執(zhí)行。

          function foo() {
           var a=2;
           function bar() {
           console.log( a ); // 2
           }
           bar();
          }
          foo();
          

          按照第一種定義,這個(gè)就是閉包了,因?yàn)樵谝粋€(gè)函數(shù)foo內(nèi)部創(chuàng)建另一個(gè)函數(shù)bar()。其實(shí),我們仔細(xì)看下定義就會(huì)發(fā)現(xiàn):在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù)是創(chuàng)建閉包的常見(jiàn)方式,并不是閉包的定義。確切的說(shuō),上述代碼中bar() 對(duì)a 的引用的方法是詞法作用域的查找規(guī)則,而這些規(guī)則只是閉包的一部分。

          var a=2;
          (function IIFE() {
           console.log( a );//2
          })();
          

          這個(gè)是閉包嗎?按照前面的定義,并不是,因?yàn)镮IFE這個(gè)函數(shù)并不是在它本身的詞法作用域以外執(zhí)行的,a 是通過(guò)普通的詞法作用域查找而非閉包被發(fā)現(xiàn)的。

          function foo() {
           var a=2;
           function bar() {
           console.log( a );
           }
           return bar;
          }
          var baz=foo();
          baz(); // 2 
          

          在上面例子中,bar()在自己定義的詞法作用域以外的地方被執(zhí)行,這就是閉包。

          一般情況下,由于有垃圾回收機(jī)制,在foo() 執(zhí)行后,foo() 的整個(gè)內(nèi)部作用域都被銷(xiāo)毀。而閉包的“神奇”之處在于可以阻止這件事情的發(fā)生。事實(shí)上,bar()在使用foo() 的內(nèi)部作用域,所以這個(gè)內(nèi)部作用域依然存在,拜bar() 所聲明的位置所賜,它擁有涵蓋foo() 內(nèi)部作用域的閉包,使得該作用域能夠一直存活,使得bar() 在之后任何時(shí)間進(jìn)行引用。bar() 對(duì)foo()的作用域的引用,就叫作閉包。

          function foo() {
           var a=2;
           function baz() {
           console.log( a ); // 2
           }
           bar( baz );
          }
          function bar(fn) {
           fn(); 
          }
          var fn;
          function foo() {
           var a=2;
           function baz() {
           console.log( a );
           }
           fn=baz; // 將baz 分配給全局變量
          }
          function bar() {
           fn(); // 這就是閉包!
          }
          foo();
          bar(); // 2
          

          上述兩段代碼的區(qū)別在于,函數(shù)值的傳遞方式不同,但其運(yùn)行結(jié)果一樣,而且都產(chǎn)生了閉包。因此,無(wú)論通過(guò)何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會(huì)持有對(duì)原始定義作用域的引用,無(wú)論在何處執(zhí)行這個(gè)函數(shù)都會(huì)使用閉包。

          我們?cè)賮?lái)分析閉包中經(jīng)典的for循環(huán)問(wèn)題

          for(var i=0;i<5;i++){
           setTimeout(function timer(){
           console.log( i );
           },i*1000)
          }
          

          如果你認(rèn)為這段代碼的運(yùn)行結(jié)果為分五次輸出0,1,2,3,4,每次間隔為1秒,那就錯(cuò)了。正確的結(jié)果是,五次輸出都為5,那么,這個(gè)5 是從哪里來(lái)的呢?我們發(fā)現(xiàn)這個(gè)循環(huán)的終止條件是i >=5。條件首次成立時(shí)i 的值是5。因此,輸出顯示的是循環(huán)結(jié)束時(shí)i 的最終值。

          作用域鏈

          function compare(value1, value2){
           if (value1 < value2){
           return -1;
           } else if (value1 > value2){
           return 1;
           } else {
           return 0;
           }
          }
          var result=compare(5, 10);
          

          以上代碼先定義了compare()函數(shù),然后又在全局作用域中調(diào)用了它。當(dāng)調(diào)用compare()時(shí),會(huì)創(chuàng)建一個(gè)包含arguments、value1 和value2 的活動(dòng)對(duì)象。全局執(zhí)行環(huán)境的變量對(duì)象(包含result和compare)在compare()執(zhí)行環(huán)境的作用域鏈中則處于第二位。下圖展示了包含上述關(guān)系的compare()函數(shù)執(zhí)行時(shí)的作用域鏈。

          作用域鏈本質(zhì)上是一個(gè)指向變量對(duì)象的指針列表,它只引用但不實(shí)際包含變量對(duì)象。

          無(wú)論什么時(shí)候在函數(shù)中訪問(wèn)一個(gè)變量時(shí),就會(huì)從作用域鏈中搜索具有相應(yīng)名字的變量。一般來(lái)講,當(dāng)函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就會(huì)被銷(xiāo)毀,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對(duì)象)。但是,閉包的情況又有所不同。

          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;
           }
           };
          }
          

          在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會(huì)將包含函數(shù)(即外部函數(shù))的活動(dòng)對(duì)象添加到它的作用域鏈中。因此,在createComparisonFunction()函數(shù)內(nèi)部定義的匿名函數(shù)的作用域鏈中,實(shí)際上將會(huì)包含外部函數(shù)createComparisonFunction()的活動(dòng)對(duì)象。這段代碼的作用域鏈如下所示

          在匿名函數(shù)從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數(shù)的活動(dòng)對(duì)象和全局變量對(duì)象。這樣,匿名函數(shù)就可以訪問(wèn)在createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()函數(shù)在執(zhí)行完畢后,其活動(dòng)對(duì)象也不會(huì)被銷(xiāo)毀,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對(duì)象。換句話說(shuō),當(dāng)createComparisonFunction()函數(shù)返回后,其執(zhí)行環(huán)境的作用域鏈會(huì)被銷(xiāo)毀,但它的活動(dòng)對(duì)象仍然會(huì)留在內(nèi)存中;直到匿名函數(shù)被銷(xiāo)毀后,createComparisonFunction()的活動(dòng)對(duì)象才會(huì)被銷(xiāo)毀

          //創(chuàng)建函數(shù)
          var compareNames=createComparisonFunction("name");
          //調(diào)用函數(shù)
          var result=compareNames({ name: "Nicholas" }, { name: "Greg" });
          //解除對(duì)匿名函數(shù)的引用(以便釋放內(nèi)存)
          compareNames=null;
          

          首先,創(chuàng)建的比較函數(shù)被保存在變量compareNames 中。而通過(guò)將compareNames 設(shè)置為等于null解除該函數(shù)的引用,就等于通知垃圾回收例程將其清除。隨著匿名函數(shù)的作用域鏈被銷(xiāo)毀,其他作用域(除了全局作用域)也都可以安全地銷(xiāo)毀了。

          內(nèi)存管理

          在閉包中調(diào)用局部變量,會(huì)導(dǎo)致這個(gè)局部變量無(wú)法及時(shí)被銷(xiāo)毀,相當(dāng)于全局變量一樣會(huì)一直占用著內(nèi)存。如果需要回收這些變量占用的內(nèi)存,可以手動(dòng)將變量設(shè)置為null。

          然而在使用閉包的過(guò)程中,比較容易形成 JavaScript 對(duì)象和 DOM 對(duì)象的循環(huán)引用,就有可能造成內(nèi)存泄露。這是因?yàn)闉g覽器的垃圾回收機(jī)制中,如果兩個(gè)對(duì)象之間形成了循環(huán)引用,那么它們都無(wú)法被回收。

          function func() {
           var test=document.getElementById('test');
           test.onclick=function () {
           console.log('hello world');
           }
          }
          

          在上面例子中,func 函數(shù)中用匿名函數(shù)創(chuàng)建了一個(gè)閉包。變量 test 是 JavaScript 對(duì)象,引用了 id 為 test 的 DOM 對(duì)象,DOM 對(duì)象的 onclick 屬性又引用了閉包,而閉包又可以調(diào)用 test(test.onclick函數(shù)中的this就是對(duì)象test) ,因而形成了循環(huán)引用,導(dǎo)致兩個(gè)對(duì)象都無(wú)法被回收。要解決這個(gè)問(wèn)題,只需要把循環(huán)引用中的變量設(shè)為 null 即可。

          function func() {
           var test=document.getElementById('test');
           test.onclick=function () {
           console.log('hello world');
           }
           test=null;
          }
          

          如果在 func 函數(shù)中不使用匿名函數(shù)創(chuàng)建閉包,而是通過(guò)引用一個(gè)外部函數(shù),也不會(huì)出現(xiàn)循環(huán)引用的問(wèn)題。

          function func() {
           var test=document.getElementById('test');
           test.onclick=funcTest;
          }
          function funcTest(){
           console.log('hello world');
          }
          

          應(yīng)用場(chǎng)景

          函數(shù)防抖和節(jié)流

          防抖和節(jié)流的作用都是防止函數(shù)多次調(diào)用。區(qū)別在于,假設(shè)一個(gè)用戶一直觸發(fā)這個(gè)函數(shù),且每次觸發(fā)函數(shù)的間隔小于wait,防抖的情況下只會(huì)調(diào)用一次,而節(jié)流的 情況會(huì)每隔一定時(shí)間(參數(shù)wait)調(diào)用函數(shù)。

          設(shè)計(jì)單例模式

          • 組件化思想,使用簡(jiǎn)單,擴(kuò)展簡(jiǎn)單
          • 避免全局污染
          class CreateUser {
           constructor(name) {
           this.name=name;
           this.getName();
           }
           getName() {
           return this.name;
           }
          }
          // 代理實(shí)現(xiàn)單例模式
          var ProxyMode=(function() {
           var instance=null;
           return function(name) {
           if(!instance) {
           instance=new CreateUser(name);
           }
           return instance;
           }
          })();
          // 測(cè)試單體模式的實(shí)例
          var a=ProxyMode("aaa");
          var b=ProxyMode("bbb");
          // 因?yàn)閱误w模式是只實(shí)例化一次,所以下面的實(shí)例是相等的
          console.log(a===b); //true
          

          設(shè)置私有變量

          //賦值到閉包里
          const Squery=(function () {
           const _width=Symbol();
           class Squery {
           constructor(s) {
           this[_width]=s
           }
           foo() {
           console.log(this[_width])
           }
           }
           return Squery
          })();
          const ss=new Squery(20);
          ss.foo();
          console.log(ss[_width])
          

          為節(jié)點(diǎn)循環(huán)綁定click事件

          <p id="info">123</p>
          <p>E-mail: <input type="text" id="email" name="email"></p>
          <p>Name: <input type="text" id="name" name="name"></p>
          <p>Age: <input type="text" id="age" name="age"></p>
          <script>
           function showContent(content){
           document.getElementById('info').innerHTML=content;
           };
           function setContent(){
           var infoArr=[
           {'id':'email','content':'your email address'},
           {'id':'name','content':'your name'},
           {'id':'age','content':'your age'}
           ];
           for (var i=0; i < infoArr.length; i++) {
           var item=infoArr[i];
           //循環(huán)中創(chuàng)建了三個(gè)閉包,他們使用了相同的詞法環(huán)境item,item.content是變化的變量
           //當(dāng)onfocus執(zhí)行時(shí),item.content才確定,此時(shí)循環(huán)已經(jīng)結(jié)束,三個(gè)閉包共享的item已經(jīng)指向數(shù)組最后一項(xiàng)。
           document.getElementById(item.id).onfocus=function(){
           showContent(item.content)
           }
           }
           }
           setContent()
           /**
           * 解決方法1
           * 通過(guò)函數(shù)工廠,則函數(shù)為每一個(gè)回調(diào)都創(chuàng)建一個(gè)新的詞法環(huán)境
           */
           function showContent(content){
           document.getElementById('info').innerHTML=content;
           };
           function callBack(content){
           return function(){
           showContent(content)
           }
           };
           function setContent(){
           var infoArr=[
           {'id':'email','content':'your email address'},
           {'id':'name','content':'your name'},
           {'id':'age','content':'your age'}
           ];
           for (var i=0; i < infoArr.length; i++) {
           var item=infoArr[i];
           document.getElementById(item.id).onfocus=callBack(item.content)
           }
           }
           setContent()
           /**
           * 解決方法2
           * 綁定事件放在立即執(zhí)行函數(shù)中
           */
           function showContent(content){
           document.getElementById('info').innerHTML=content;
           };
           function setContent(){
           var infoArr=[
           {'id':'email','content':'your email address'},
           {'id':'name','content':'your name'},
           {'id':'age','content':'your age'}
           ];
           for (var i=0; i < infoArr.length; i++) {
           (function(){
           var item=infoArr[i];
           document.getElementById(item.id).onfocus=function(){
           showContent(item.content)
           }
           })()//放立即執(zhí)行函數(shù),立即綁定,用每次的值綁定到事件上,而不是循環(huán)結(jié)束的值
           }
           }
           setContent()
           /**
           * 解決方案3
           * 用ES6聲明,避免聲明提前,作用域只在當(dāng)前塊內(nèi)
           */
           function showContent(content){
           document.getElementById('info').innerHTML=content;
           };
           function setContent(){
           var infoArr=[
           {'id':'email','content':'your email address'},
           {'id':'name','content':'your name'},
           {'id':'age','content':'your age'}
           ];
           for (var i=0; i < infoArr.length; i++) {
           let item=infoArr[i]; //限制作用域只在當(dāng)前塊內(nèi)
           document.getElementById(item.id).onfocus=function(){
           showContent(item.content)
           }
           }
           }
           setContent()
          

          參考文獻(xiàn):

          • 你不知道的JavaScript之閉包

          主站蜘蛛池模板: 北岛玲在线一区二区| 色一情一乱一伦一区二区三区| 精品一区二区三区在线播放 | 中文字幕日韩一区| 无码人妻精品一区二区三区久久久| 国产一区在线视频观看| www一区二区三区| 日本精品高清一区二区| 多人伦精品一区二区三区视频| 亚洲国产老鸭窝一区二区三区| 精品乱码一区二区三区在线| 国产精品制服丝袜一区| 濑亚美莉在线视频一区| 日本道免费精品一区二区| 波多野结衣电影区一区二区三区| tom影院亚洲国产一区二区| 亚洲综合色自拍一区| 男人的天堂精品国产一区| 亚洲一区欧洲一区| 天码av无码一区二区三区四区| 国产成人欧美一区二区三区| 国产一区二区三区内射高清| 日本成人一区二区| 相泽南亚洲一区二区在线播放| 中文字幕无线码一区二区| 亚洲国产高清在线一区二区三区 | 无码福利一区二区三区| 国产一区二区在线观看视频| 亚洲精品伦理熟女国产一区二区 | 国产福利电影一区二区三区,免费久久久久久久精 | 国产福利一区二区| 亚洲毛片αv无线播放一区| 国模大胆一区二区三区| 国产怡春院无码一区二区| 成人无码精品一区二区三区| 亚洲一区二区三区在线观看网站| 日韩精品中文字幕无码一区| 免费在线视频一区| 人妖在线精品一区二区三区| 香蕉免费看一区二区三区| 亚洲欧美日韩一区二区三区在线|