Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
文面向初學者,大神輕噴。
好了,開始吧。
上一節 JavaScript: 零基礎輕松學閉包(1)中,我們對閉包的原理進行了講解,這一節會說很多實戰性的東西了,可能會有點難度,你準備好了嗎?
還記得在上一節中,有這樣一個例子么?
var test=function(){ var i=10; } function test2(){ alert(i); } test2();
函數 test 和 test2 各自形成一個閉包,兩個閉包之間無法訪問對方的私有數據。比如,在 test 中定義的變量,在 test2 里面是無法直接訪問到的。
那么問題來了, 當然,這邊和挖掘機沒關系。這里的問題是,有沒有什么辦法讓 test2 可以訪問到其他閉包中的私有變量呢?
辦法當然是有的,最直接的想法就是,大不了我定義一個全局變量,在 test 中將私有數據賦給全局變量,然后在 test2 里面就能訪問到了。
是的,因為兩個函數共同享有一個全局作用域,所以這個辦法確實可行。我在很多項目里也的確看到很多人就是這么做的。
那么,有沒有一種更好的方法呢?要知道,全局作用域是一個比較敏感的地方,一不小心就會出現變量名重復的問題。順便說一句,在全局作用域中,盡量不要使用諸如 temp , a , b , c 這一類的大眾化變量。
于是,這就牽扯到返回值的相關知識了,你在C語言的教材中肯定見慣了類似于這樣的代碼
int sum(int a,int b) { return a + b; } int all=sum(3,5);
這是一個簡單的求和函數,很多人慢慢地養成了這樣一個觀念,就是函數的返回值就是一個字面值,要么是數字類型,要么是布爾類型,或者是字符串。
在很多強類型的語言,諸如 Java,C,C++, 確實如此。但是 return 在 JavaScript 中卻大有來頭。
在上一節已經說明了,js 的函數也是一種數據類型,你可以把函數看成是和int , float , double 一樣的東西。
那么,既然int可以當做函數的參數或者返回值,函數當然也可以!
請看下面兩句話:
在js中
這一章不講回調函數,如果你不清楚啥叫回調函數,可以去看看這個小例子:
(淺談js回調函數)
還是上面的那個例子,我們希望在 test2 中可以訪問到 test 里面的變量,可以這樣做:
var test=function(){ var i=10; /* 定義一個函數將變量i暴露出去 */ var get=function(){ return i ; } return get; //將獲得i的函數暴露出去 } function test2(){ var fn=test();//接收test暴露出來的函數 alert(fn()); //獲得test中的私有數據 } test2();
test 函數中的 get 方法是一個內部函數,它自己也形成了一個閉包, test 是他的父級作用域,因此它可以獲取i的值。
i 進入 get 方法的閉包,被包了起來,然后最終被返回了出去。
而對于 test2 來說,是可以訪問到 test函數的,因此可以調用并執行 test 函數,從而獲取其返回值。
你可能會說,我直接在test中把i給return出去就好了嘛,干嘛這么麻煩。
是的,言之有道理。
可是,如果我要訪問 test 中多個私有數據咋辦捏?
這下你可明白了吧!
現在,我們給出關于閉包的第二個注解:
(第一個注解在上一節)
從應用的角度來看,閉包可以將函數或者對象的私有數據暴露出去,而不影響全局作用域。
通過這張圖,是不是好理解一些了呢?我們這一節單說函數里的私有數據。
2. 將私有數據包裝成json對象
剛才的例子說明,在js中,return出去的可以是基本數據類型,也可以是函數類型。
其實,JavaScript是一種基于對象的語言,也有對象的概念,所以,我們可以把你需要的東西包裹成一個對象返回出去!
上代碼:
var test=function(){ var apple='蘋果'; var pear='梨子'; /* 定義一個函數將水果暴露出去 */ var getFruit={ apple : apple , pear : pear } return getFruit; //將獲得i的函數暴露出去 } function test2(){ var getFruit=test();//接收test暴露出來的函數 console.log(getFruit); } test2();
像這樣用 { } 括起來的東西就是一個js對象,也就是所謂json。你可能經常會聽到json這個詞,覺得還挺高大上的。其實它就是一個用 { } 包起來的數據而已。
里面是鍵值對的形式,非常類似于Java里面的HashMap。
在這個例子中,我們可以直接把需要暴露的私有數據用一個 { } 包起來,構成一個json對象return出去就可以啦。
因為是 js 對象,alert 不能看到里面的具體內容,所以我們使用 console.log() ,結果如下:
展開后:
Paste_Image.png
這樣是不是也可以了?多出來的 proto 是原型鏈,以后會講到。
大家都還記得西游記里孫悟空用遮天的把戲騙來的紫金葫蘆嗎,只要你拿著這個葫蘆,叫一聲別人的名字,如果答應了,別人就會被吸進去。
OK,這個紫金葫蘆里面不正如一個閉包嗎?
對不對嘛,所以,我們用閉包的知識來做一個好玩的東西吧。
<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; }
注:我純粹是為了看起來方便而采用中文定義變量,在實際開發中,千萬不要使用中文變量。
我們在返回出去的對象上加了三個東西:
1.domElement
你傳進來一個id,我就用 document.getElementById 來包一下,得到一個dom元素,最終要操作的也就是這個dom元素。也就是說:
var box1=紫金葫蘆('box').domElement; var box2=document.getElementById('box'); alert(box1===box2);
他們是一個東西,一樣的。
紫金葫蘆('box');
這行代碼一旦執行,紫金葫蘆就會返回 returnObject 對象,也就是說。我們喊一聲 “box”,那個id為box的小妖一答應,就被裝進來了,然后我們可以對它為所欲為!
比如,給它換一個背景色:
2.backgroundColor 給元素添加背景色的方法
var box=紫金葫蘆('box'); box.backgroundColor('red');
3.click 給元素添加點擊事件,需要傳入一個回調函數
var box=紫金葫蘆('box'); box.click(function(){ alert('就沒人吐槽這個無聊的作者么,小妖也有尊嚴的好么,啊喂?。?); });
結果:
也許你已經發現了,這些方法是不是和jQuery有點類似呢?
(1)變量的作用域 不帶有關鍵字var的變量會成為全局變量; 在函數中使用關鍵字var聲明的變量是局部變量。 局部變量只有在函數內部才能訪問到,在函數外面是訪問不到的。但在函數內部可以通過作用域鏈一直向上搜索直到全局對象,也就是說,函數內部可以訪問函數外部的變量。
(2)變量的生存周期 對于全局變量,其生存周期是永久的,除非主動銷毀這個全局變量; 而對于在函數內用關鍵字var聲明的局部變量,當退出函數時,這些局部變量會隨著函數調用結束而被銷毀。
例外情況:閉包
問題:無論單擊哪個div,都會彈出5。 原因:onclick事件是異步觸發的,當事件被觸發時,for循環早已結束,此時變量i的值早已經是5。 解決:在閉包的幫助下,把每次循環的i值都封閉起來。當事件函數順著作用域鏈從內到外查找變量i時,會先找到被封閉在閉包環境的i,單擊div時,會分別輸出0,1,2,3,4。
類似實例:閉包直接賦給數組
結果:result的每個元素都返回10。 說明:閉包的作用域鏈有明顯的副作用——閉包總是獲得外部函數變量的最終值。上面代碼中,外部函數產生一個函數數組result并返回。函數數組中的每個元素都是一個函數,每個函數都返回 i變量。似乎每個函數應該返回每次循環的i值,即依次返回0到9,但事實是,每個函數的返回結果都是10。這是因為每個內部函數返回的是變量i,而不是i在某個時刻的特定值,而i的作用域是整個外部函數,當外部函數執行完成后,i的值是10。 解決:在每個內部函數的內部,再產生一個匿名函數并返回。
結果:result依次返回0到9。 說明:(i)使得該層匿名函數立即執行。 ###3、閉包
有時候需要得到函數內的局部變量。如何從外部讀取局部變量?那就是在函數的內部,再定義一個函數。 閉包是指有權訪問另一個函數作用域中變量的函數,創建閉包的最常見的方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量,利用閉包可以突破作用鏈域,將函數內部的變量和方法傳遞到外部。 ① 閉包的原理
在上面的代碼中,函數f2就被包括在函數f1內部,這時f1內部的所有局部變量,對f2都是可見的。但是反過來就不行,f2內部的局部變量,對f1就是不可見的。既然f2可以讀取f1中的局部變量,那么只要把f2作為返回值,就可以在f1外部讀取它的內部變量了。
閉包就是能夠讀取其他函數內部變量的函數。由于在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函數f1中的局部變量n一直保存在內存中,并沒有在f1調用后被自動清除。原因就在于f1是f2的父函數,而f2被賦給了一個全局變量,這導致f2始終在內存中,而f2的存在依賴于f1,因此f1也始終在內存中,不會在調用結束后,被垃圾回收機制回收。 (2)讓函數內部的變量的值始終保持在內存中(延長局部變量的壽命)
由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。也就是說,閉包會引用外部函數作用域,會占用更多的內存,過度使用閉包,會導致性能問題。所以,僅當必要時才使用閉包。對產生閉包的函數,使用后應該解除引用。 (3)自執行函數+閉包減少全局變量污染(封裝私有變量)
var person=(function() { var_name="Alice"; var _id=16; return { getUserInfo: function() { return _name + ": " + _id; } } })();//幫助突破技術瓶頸,提升思維能力
使用下劃線來約定私有變量_name和_age,它們被封裝在閉包產生的作用域中,外部是訪問不到這兩個變量的,這就避免了對全局的命令污染。 ④ 閉包的缺點: (1) 需要維護額外的作用域。 (2) 過渡使用閉包會占用大量內存。 4、this對象 在閉包內使用this對象將產生一些復雜的行為。this對象的值基于函數所在的執行環境在運行時決定:在全局函數中使用時,this等于window(非嚴格模式)或undefined(嚴格模式);而當作為對象的方法調用時,this等于這個對象。
每個函數一旦被調用,它將自動獲得this和arguments兩個變量。一個內部函數是不能直接從外部函數訪問到這兩個變量的??梢酝ㄟ^將this對象存儲在另一個變量中來解決這個問題。把外部作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了。
var name="The window"; var object={ name: "My object", getName: function() { var that=this; return function() { return that.name; }; } }; alert(object.getName()());//輸出:"My object"
要讓閉包訪問外部函數的this和arguments對象,可以通過將它們的引用存儲在另一個變量中來完成。 5、內存泄漏 使用閉包的時候很容易造成循環引用,若閉包的作用域包含著一些DOM節點,這時候就有可能造成內存泄漏,但其實,這本身不是閉包的問題,而是由于:BOM和DOM中的對象是使用C++以COM對象的方式實現的,而COM對象的垃圾收集機制采用的是引用計數策略,在基于引用計數策略的垃圾回收機制中,若兩個對象之間形成了循環引用,則這兩個對象都無法被回收。
匿名函數保存了一個對element的引用,只要匿名函數存在,element的引用數至少為1,它所占用的內存就永遠不會被回收。
通過把element.id的一個副本保存在變量中,并且在閉包中引用該變量消除了循環引用,但是僅僅做到這一步還不能解決內存泄漏的問題,閉包會引用包含函數的所有活動對象,包含element,即使閉包不直接引用element,包含函數的活動對象中也仍然會保存一個引用,因此有必要把element元素設置為null,這樣就能解除對DOM對象的引用,確保正?;厥掌湔加玫膬却妗?6、模仿塊級作用域 JavaScript中沒有直接的塊級作用域。
使用閉包可以模仿塊級作用域——創建并立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。結果是內部函數的所有變量都會立即被銷毀,除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。 用作塊級作用域的匿名函數:將函數聲明包含在一對圓括號中,表示它實際上是一個函數表達式,緊隨其后的另一對圓括號會立即調用這個函數。
(function() { //這里是塊級作用域; }) ();
可以使用匿名函數表達式來模擬塊級作用域,任何定義在匿名函數中的變量在匿名函數執行完之后都將被銷毀,在匿名函數外訪問這些變量將會產生錯誤。
喜歡的小伙伴,轉發+點個關注吧!
包是什么
閉包是指那些能夠訪問自由變量的函數。
《JavaScript高級程序設計第三版》:閉包是指有權訪問另一個函數作用域中的變量的函數,創建閉包的常見方式,就是在一個函數內部創建另一個函數。
《你不知道的JavaScript(上卷)》:當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。
function foo() { var a=2; function bar() { console.log( a ); // 2 } bar(); } foo();
按照第一種定義,這個就是閉包了,因為在一個函數foo內部創建另一個函數bar()。其實,我們仔細看下定義就會發現:在一個函數內部創建另一個函數是創建閉包的常見方式,并不是閉包的定義。確切的說,上述代碼中bar() 對a 的引用的方法是詞法作用域的查找規則,而這些規則只是閉包的一部分。
var a=2; (function IIFE() { console.log( a );//2 })();
這個是閉包嗎?按照前面的定義,并不是,因為IIFE這個函數并不是在它本身的詞法作用域以外執行的,a 是通過普通的詞法作用域查找而非閉包被發現的。
function foo() { var a=2; function bar() { console.log( a ); } return bar; } var baz=foo(); baz(); // 2
在上面例子中,bar()在自己定義的詞法作用域以外的地方被執行,這就是閉包。
一般情況下,由于有垃圾回收機制,在foo() 執行后,foo() 的整個內部作用域都被銷毀。而閉包的“神奇”之處在于可以阻止這件事情的發生。事實上,bar()在使用foo() 的內部作用域,所以這個內部作用域依然存在,拜bar() 所聲明的位置所賜,它擁有涵蓋foo() 內部作用域的閉包,使得該作用域能夠一直存活,使得bar() 在之后任何時間進行引用。bar() 對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
上述兩段代碼的區別在于,函數值的傳遞方式不同,但其運行結果一樣,而且都產生了閉包。因此,無論通過何種手段將內部函數傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。
我們再來分析閉包中經典的for循環問題
for(var i=0;i<5;i++){ setTimeout(function timer(){ console.log( i ); },i*1000) }
如果你認為這段代碼的運行結果為分五次輸出0,1,2,3,4,每次間隔為1秒,那就錯了。正確的結果是,五次輸出都為5,那么,這個5 是從哪里來的呢?我們發現這個循環的終止條件是i >=5。條件首次成立時i 的值是5。因此,輸出顯示的是循環結束時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()函數,然后又在全局作用域中調用了它。當調用compare()時,會創建一個包含arguments、value1 和value2 的活動對象。全局執行環境的變量對象(包含result和compare)在compare()執行環境的作用域鏈中則處于第二位。下圖展示了包含上述關系的compare()函數執行時的作用域鏈。
作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同。
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; } }; }
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。這段代碼的作用域鏈如下所示
在匿名函數從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局變量對象。這樣,匿名函數就可以訪問在createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。換句話說,當createComparisonFunction()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀
//創建函數 var compareNames=createComparisonFunction("name"); //調用函數 var result=compareNames({ name: "Nicholas" }, { name: "Greg" }); //解除對匿名函數的引用(以便釋放內存) compareNames=null;
首先,創建的比較函數被保存在變量compareNames 中。而通過將compareNames 設置為等于null解除該函數的引用,就等于通知垃圾回收例程將其清除。隨著匿名函數的作用域鏈被銷毀,其他作用域(除了全局作用域)也都可以安全地銷毀了。
內存管理
在閉包中調用局部變量,會導致這個局部變量無法及時被銷毀,相當于全局變量一樣會一直占用著內存。如果需要回收這些變量占用的內存,可以手動將變量設置為null。
然而在使用閉包的過程中,比較容易形成 JavaScript 對象和 DOM 對象的循環引用,就有可能造成內存泄露。這是因為瀏覽器的垃圾回收機制中,如果兩個對象之間形成了循環引用,那么它們都無法被回收。
function func() { var test=document.getElementById('test'); test.onclick=function () { console.log('hello world'); } }
在上面例子中,func 函數中用匿名函數創建了一個閉包。變量 test 是 JavaScript 對象,引用了 id 為 test 的 DOM 對象,DOM 對象的 onclick 屬性又引用了閉包,而閉包又可以調用 test(test.onclick函數中的this就是對象test) ,因而形成了循環引用,導致兩個對象都無法被回收。要解決這個問題,只需要把循環引用中的變量設為 null 即可。
function func() { var test=document.getElementById('test'); test.onclick=function () { console.log('hello world'); } test=null; }
如果在 func 函數中不使用匿名函數創建閉包,而是通過引用一個外部函數,也不會出現循環引用的問題。
function func() { var test=document.getElementById('test'); test.onclick=funcTest; } function funcTest(){ console.log('hello world'); }
函數防抖和節流
防抖和節流的作用都是防止函數多次調用。區別在于,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小于wait,防抖的情況下只會調用一次,而節流的 情況會每隔一定時間(參數wait)調用函數。
設計單例模式
class CreateUser { constructor(name) { this.name=name; this.getName(); } getName() { return this.name; } } // 代理實現單例模式 var ProxyMode=(function() { var instance=null; return function(name) { if(!instance) { instance=new CreateUser(name); } return instance; } })(); // 測試單體模式的實例 var a=ProxyMode("aaa"); var b=ProxyMode("bbb"); // 因為單體模式是只實例化一次,所以下面的實例是相等的 console.log(a===b); //true
設置私有變量
//賦值到閉包里 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])
為節點循環綁定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]; //循環中創建了三個閉包,他們使用了相同的詞法環境item,item.content是變化的變量 //當onfocus執行時,item.content才確定,此時循環已經結束,三個閉包共享的item已經指向數組最后一項。 document.getElementById(item.id).onfocus=function(){ showContent(item.content) } } } setContent() /** * 解決方法1 * 通過函數工廠,則函數為每一個回調都創建一個新的詞法環境 */ 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 * 綁定事件放在立即執行函數中 */ 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) } })()//放立即執行函數,立即綁定,用每次的值綁定到事件上,而不是循環結束的值 } } setContent() /** * 解決方案3 * 用ES6聲明,避免聲明提前,作用域只在當前塊內 */ 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]; //限制作用域只在當前塊內 document.getElementById(item.id).onfocus=function(){ showContent(item.content) } } } setContent()
參考文獻:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。