這篇文章為大家帶來了關於JavaScript閉包的學習筆記,其中包括了閉包與方法堆疊以及閉包的作用,希望對大家有幫助。
從定義上來講,它是一個腳本語言,而且是一個相對容易學習的腳本語言。不需要太多的專業知識,你也能夠在某種程度上使用js(JavaScript的簡寫)程式碼。
當然如果你已經學習了一前端知識,你應該能理解這個工具的作用,這是一個非常方便的顯示頁面元素之間間距的工具。你看,你只是進行了一些簡單的瀏覽器操作,甚至你無法理解上述程式碼的內容,但你剛剛確確實實的嵌入了一段js程式碼在你所在的頁面中(顯然它是無害的,請放心使用)感謝up主CodingStartup起碼課的影片【有了它,把網頁做到跟設計圖一樣】以及up主ArcRain在影片下方的回應
這篇學習筆記的目的是記錄我自己對於js學習路程中的一些感悟和體會,以及一些我自己認為的小技巧,而不是為了教學,所以其中的部分內容的原理我並不會給出答案,有可能是我沒法準確的描述,有可能是我還沒搞懂,本人程度相當有限,如果文字中有錯誤的部分歡迎大家指摘。
正式學習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学习教程
以上是你必須了解的JavaScript閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!