您好,登錄后才能下訂單哦!
這篇文章主要介紹“JavaScript閉包怎么理解”,在日常操作中,相信很多人在JavaScript閉包怎么理解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JavaScript閉包怎么理解”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
從定義上來講,它是一個腳本語言,而且是一個相對容易學習的腳本語言。不需要太多的專業知識,你也能夠在一定程度上使用js(JavaScript的簡寫)代碼。
正式學習JavaScript是在培訓班,沒錯我是從培訓班出來的,并不是科班出身,可以說是非常的草根了。我學習的時候ES6標準還并未普及,變量命名還在用非常傳統的var,學習的第一段代碼是經典的console.log('Hello,world!'),當然它是在控制臺上打印出來的。
當然,在培訓機構中的JavaScript內容講的是非常的淺顯,只有最為基礎的變量定義與命名,function聲明,回調函數,ajax以及最為基礎的dom操作。顯然這些內容對于工作完全不夠用的。
對于js學習的‘進修’機會來源于我的工作,在工作中我第一次知道了node這個東西,也了解到即便是js也是可以做后臺的(我是做的JAVA培訓),也開始逐漸接觸到了一些ES6的標準。當然這些都是后話,最開始我接觸到最大的障礙是這貨。
啊,對我只有那么一丁丁點基礎的我,完全無法理解我們公司自己封裝的jsonp代碼,它是長這個樣子的。
var jsonp = (function(){ var JSONP; return function(url){ if (JSONP) { document.getElementsByTagName("head")[0].removeChild(JSONP); } JSONP = document.createElement("script"); JSONP.type = "text/javascript"; JSONP.src = url; document.getElementsByTagName("head")[0].appendChild(JSONP); } }())
當然,現在瀏覽器上已經無法通過控制臺直接使用這個方法了,為了防止XSS攻擊瀏覽器已經禁止這樣注入代碼了,但是在服務器上還是可以用的,當然,這些都不是重點。
重點是這里
if (JSONP) { //dosome }
如果你和我當初一樣,不知道什么叫閉包或者對閉包一知半解,那么,對于這里你應該也會產生疑問,思路大約是這樣的
第2行定義了JSONP但是沒有賦值,現在JSONP值為null,第三行返回了一個方法,第四行檢測JSONP值是否為空,如果不為空則做了一些事情,好了,后面可以不用看了,這個if白寫了,它百分百進不去!
你看嘛,前面也沒有賦值,然后直接判斷,那它明明就是null。但是實際使用的時候你會發現,這個地方第一次調用確實不會進入這個分支,但只要你調用了第二次,,它就百分百會進入這個分支。
// 這個是一個可以在控制臺輸出的閉包版本,你可以自己試一下 var closedhull = (function() { let name = null; // 這里直接賦值為null return function(msg){ if(name) { console.log('name:', name) return name += msg; } return name = msg; } }()) closedhull('我是第一句。') //我是第一句。 closedhull('我是第二句。') //我是第一句。我是第二句。
上面這個例子運行后,無論是從console.log()亦或是返回值上都不難看出,確實進入了if(name)的分支,這個就是閉包的表現。這里給出一下閉包的定義
閉包就是能夠讀取其他函數內部變量的函數。
好了,看過閉包是個啥了,先不說會不會用,至少,算是見過了,閉包有個顯著的特征return function(){}
不是!
它的顯著特征是在function內的function!
觀察以下方法
/*第一個案例*/ function test1(){ // a應該在方法運行結束后銷毀 let a = 1; return { add: function(){ return ++a; } } } let a = test1(); a.add()//2 a.add()//3 /*第二個案例*/ (function(){ // b應該在方法運行結束后銷毀 let b = 1, timer = setInterval(()=>{ console.log(++b) }, 2000) setTimeout(()=>{ clearInterval(timer) }, 10000) })()// 2 3 4 5 6 /*第三個案例*/ function showMaker(obj){ // obj應該在方法運行結束后銷毀 return function(){ console.log(JSON.stringify(obj)) } } let shower = showMaker({a:1}) // 顯然這里你還能看到他 shower(); // {"a":1} /*第四個案例*/ let outObj = (function(){ let c = 'hello', obj = {}; Object.defineProperty(obj, 'out', { get(){ return c; }, set(v){ c = v; } }); return obj })() outObj.out // 可以讀取并設置c的值
這四個都是閉包,他們都具備方法中的方法這一特性。
閉包的定義,1. 可以在變量的作用域外訪問該變量。2. 通過某種手段延長一個局部變量的生命周期。3. 讓一個局部變量的存活時間超過它的時間循環執行時間
3中由于涉及到了事件循環概念,之后涉及到時會去講的,這里主要討論前兩種方式的定義。
一下內容如果你知道方法棧是個啥了就可以跳過了
局部作用域:在ES6之前,一般指一個方法內部(從參數列表開始,到方法體的括號結束為止),ES6中增加let關鍵字后,在使用let的情況下是指在一個{}中的范圍內(顯然,你不能在隱式的{}中使用let,編譯器會禁止你做出這種行為的,因為沒有{}就沒有塊級作用域),咱們這里為了簡化討論內容,暫且不把let的塊級作用域算作閉包的范疇(其實應該算,不過意義不大,畢竟,你可以在外層塊聲明它。天啊,JS的命名還沒擁擠到需要在一個方法內再去防止污染的程度。)
局部變量:區別于全局變量,全局變量會在某些時候被意外額創造和使用,這令人非常的...惱火和無助。局部變量就是在局部作用域下使用變量聲明關鍵字聲明出來的變量,應該很好理解。
局部變量的生命周期:好了,你在一個局部作用域中通過關鍵字(var const let等)聲明了一個變量,然后給它賦值,這個局部變量在這個局部作用域中冒險就開始了,它會被使用,被重新賦值(除了傲嬌的const小姐外),被調用(如果它是個方法),這個局部變量的本質是一個真實的值,區別在于如果它是個對象(對象,數組,方法都是對象)那么,它其實本質是一個地址的指針。如果它一個基礎類型,那么它就是那個真實的值。它之所以存活是因為它有個住所。內存。
局部作用域與內存:每當出現一個局部作用域,一個方法棧就被申請了出來,在這個方法棧大概長這樣子
| data5 | | data4 | | data3 | | data2 | |__data1_|
當然,它是能夠套娃的,長這個樣子
| | d2 | | | |_d1_| | | data3 | | data2 | |__data1___|
如果上面的東西是在太過于抽象,那么,我可以用實際案例展示一下
function stack1(){ var data1, data2, data3, data4, data5 } function stack2(){ var data1, data2, data3; function stackInner(){ var d1, d2; } }
如果方法棧能夠直觀的感受的話,大約就是這個樣子,咱們重點來分析stack2的這種情況,同時寫一點實際內容進去
function stack2(){ var data1 = '1', data2 = {x: '2'}, data3 = '3'; function stackInner(){ var d1 = '4', d2 = {y: '5'}; } stackInner() } stack2()
顯然其中data1,data3,d1持有的是基本類型(string),data2,d2持有的是引用類型(object),反應到圖上
運行時的方法棧的樣子
|------>{y: '5'} | |->{x: '2'} | | d2-| || | | |_d1='4'_|| | | data3='3' | | | data2 ----| | |__data1='1'___|
畫有點抽象...就這樣吧。具體對象在哪呢?他們在一個叫堆的地方,不是這次的重點,還是先看方法棧內的這些變量,運行結束后,按照先進后出的原則,把棧內的局部變量一個一個的銷毀,同時堆里的兩個對象,由于引用被銷毀,沒了繼續存在的意義,等待被垃圾回收。
接下來咱們要做兩件事情:
d1不再等于4了,而是引用data1
return stackInner 而不是直接調用
這樣閉包就完成了
function stack2(){ var data1 = {msg: 'hello'}, data2 = {x: '2'}, data3 = '3'; function stackInner(){ var d1 = data1, d2 = {y: '5'}; } return stackInner } var out = stack2()
這里有一個要點,d2賦值給data1一定是在stackInner中完成的,原因?因為再stackInner方法中d2才被聲明出來,如果你在stack2中d1 = data1那么恭喜你,你隱式的聲明了一個叫d1的全局變量,而且在stackInner由于變量屏蔽的原因,你也看不到全局上的d2,原本計劃的閉包完全泡湯。
變量屏蔽:不同作用域中相同名稱的變量就會觸發變量屏蔽。
看看棧現在的樣子
運行時的方法棧的樣子
|------>{y: '5'} out<---| | |----| | | | d2-| | | | | | |--|_d1---|_| | | | data3='3' | | | data2(略) | | |_____data1<------|__|
好了,這個圖可以和我們永別了,如果有可能,我后面會用畫圖工具替代,這么畫圖實在是太過邪典了。
這里涉及到了方法棧的一個特性,就是變量的穿透性,外部變量可以在內部的任意位置使用,因為再內部執行結束前,外部變量會一直存在。
由于stackInner被外部的out引用,導致這個對象不會隨著方法棧的結束而銷毀,接下來,最神奇的事情來了,由于stackInner這對象沒有銷毀,它內部d1依然保有data1所對應數據的引用,d1,d2一定會活下來,因為他們的爸爸stackInner活下來了,data1也以某種形式活了下來。
為什么說是某種形式,因為,本質上來說data1還是被銷毀了。沒錯,只不過,data1所引用的那個對象的地址鏈接沒有被銷毀,這個才是本質。棧在調用結束后一定是會銷毀的。但是調用本體(方法對象)只要存在,那么內部所引用的鏈接就不會斷。
這個就是閉包的成因和本質。
OK,我猜測上一個章節估計很多人都直接跳過了,其實,跳過影響也不多,這個部分描述一下結論性的東西,閉包的作用。
它的最大作用就是給你的變量一個命名空間,防止命名沖突。要知道,你的框架,你export的東西,你import進來的東西,在編譯的時候都會變成閉包,為的就是減少你變量對全局變量的污染,一個不依賴與import export的模塊的代碼大概長這個樣子
(function(Constr, global){ let xxx = new Constr(env1, env2, env3) global.NameSpace = xxx; })(function(parm1, parm2, parm3) { //dosomeing reutrn { a: 'some1', b: 'some2', funcC(){ //dosome }, funcD(){ //dosome } } }, window)
當然這種封裝代碼的風格有多種多樣的,但是大家都盡量把一套體系的內容都放到一個命名空間下,避免與其他框架產生沖突
到此,關于“JavaScript閉包怎么理解”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。