摘要:所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。所以本文中將以維基百科中的定義為準即在計算機科學中,閉包,又稱詞法閉包或函數閉包,是引用了自由變量的函數。
閉包(closure)是JavaScript中一個“神秘”的概念,許多人都對它難以理解,我也一直處于似懂非懂的狀態,前幾天深入了解了一下執行環境以及作用域鏈,可戳查看詳情,而閉包與作用域及作用域鏈的關系密不可分,所以就再深入去理解了一番。
詞法作用域Lexical Scope首先我們來理解一下作用域的概念:
通常來說,一段程序代碼中所用到的標識符并不總是有效/可用的,而限定這個標識符的可用性的代碼范圍就是這個標識符的作用域
作用域有詞法作用域與動態作用域之分,詞法作用域也可稱為靜態作用域,這樣與動態作用域看起來更對應。
詞法作用域在詞法分析階段就確定了作用域,之后不會再改變;也就是說詞法作用域是由你把代碼寫在哪里來決定的,與之后的運行情況無關
動態作用域在運行時根據程序的流程信息來動態確定作用域;也就是說動態作用域與運行情況有關
大部分編程語言都是基于詞法作用域,其中包括JavaScript
下面我們使用代碼來說明兩者的區別(此處僅僅使用JavaScript來說明兩種情況,實際上JavaScript只基于詞法作用域)
var cc = 6; function foo() { console.log(cc); // 會輸出6還是66? } function bar() { var cc = 66; foo(); } bar();
如果是詞法作用域:會輸出6,詞法作用域在寫代碼時就靜態確定了,也就是定義foo函數的時候就確定了,foo函數的內部要訪問變量cc,由于foo的內部作用域中沒有cc變量,所以會根據作用域鏈訪問到全局中的cc變量;這與在何處調用foo函數無關。
如果是動態作用域:會輸出66,動態作用域要根據代碼的運行情況來確定,它關心foo函數在何處被調用,而不關心它定義在哪里;foo函數的內部要訪問變量cc,而foo的內部作用域中沒有cc變量時,會順著調用棧在調用 foo() 的地方查找變量cc,此處是在bar函數中調用的,所以引擎會在bar的內部作用域中查找cc變量,這個cc變量的值為66
詞法作用域鏈Lexical Scope Chainvar cc = 1; function foo() { var dd = 2; console.log(cc);//1 console.log(dd);//2 } foo(); console.log(dd); //ReferenceError: dd is not defined
上面這一段代碼中,有全局變量cc以及局部變量dd,在foo函數內部可以直接訪問全局變量cc,而在foo函數外部無法讀取foo函數內的局部變量dd。
這種結果的產生源于JavaScript的作用域鏈,也正是因為這個作用域鏈才有了生成閉包的可能。
作用域鏈這一部分在另一篇文章中有詳細介紹,可戳JavaScript基礎系列---執行環境與作用域鏈,看完可以幫助更好的理解下文
關于閉包沒有一個官方的定義,不同的書籍解讀可能有些不同
在《JavaScript權威指南》中:
是指函數變量可以被隱藏于作用域鏈之內,因此看起來是函數將變量“包裹”了起來
在《JavaScript高級程序設計》中:
閉包是指有權訪問另一個函數作用域中的變量的函數
在《你不知道的JavaScript--上卷》中:
當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用
域之外執行
在維基百科的定義:
在計算機科學中,閉包(Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。
其中自由變量指:
在函數中使用的,但既不是函數參數也不是函數的局部變量的變量
一開始我也一直糾結于閉包的定義,想確切的知道閉包是什么,但是由于沒有官方的定義,難以確定。所以本文中將以維基百科中的定義為準即:
在計算機科學中,閉包(Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。閉包的創建
根據閉包的定義我們可以看出,閉包的產生條件是函數以及該函數引用了自由變量,二者缺一不可。
而這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外這一描述是閉包的特性,使用閉包后能觀察到的一種現象,而不是閉包產生的條件。所以之前看到有些人說,需要將一個函數的內部函數返回才能算閉包的言論我覺得應該是不正確的,這應該是在使用閉包。
常說的閉包會導致性能問題,也是因為這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外這一閉包特性,按理來說,在函數 執行后,函數的整個內部作用域通常都會被銷毀,因為我們知道引擎有垃
圾回收器用來釋放不再使用的內存空間,但是閉包可以阻止這件事的發生,從而可能導致內存中保存大量的變量,從而消耗大量內存產生網頁性能問題。(注意是可以,可能而非一定)
下面我們直接來看幾個栗子:
1.如果考慮全局對象,那么引用了全局變量的函數可以看做創建了閉包,因為全局變量相對于該函數來說是自由變量
var a = 1; function fa() { console.log(a); } fa();
此處,函數fa引用了自由變量a,fa創建了閉包
2.更常見的是在一個函數內部創建另一個函數
function outer(){ var b = 2; function inner(){ console.log(b); } inner(); } outer();
此處,函數inner引用了自由變量b,inner創建了閉包。
根據JavaScript基礎系列---執行環境與作用域鏈中的描述我們可以知道,調用outer()后,會進入Function Execution Context outer的創建階段:
創建作用域鏈,outer函數的[[Scopes]]屬性被加入其中
創建outer函數的活動對象AO(作為該Function Execution Context的變量對象VO),并將創建的這個活動對象AO加到作用域鏈的最前端
確定this的值
此時Function Execution Context outer可表示為:
outerEC = { scopeChain: { pointer to outerEC.VO, outer.[[Scopes]] }, VO: { arguments: { length: 0 }, b: 2, inner: pointer to function inner(), }, this: { ... } }
接著進入Function Execution Context outer的執行階段:
當遇到inner函數定義語句,進入inner函數的定義階段,inner的[[Scopes]]屬性被確定
inner.[[Scopes]] = { pointer to outerEC.VO, pointer to globalEC.VO }
遇到inner()調用語句,進入inner函數調用階段,此時進入Function Execution Context inner的創建階段:
創建作用域鏈,inner函數的[[Scopes]]屬性被加入其中
創建inner函數的活動對象AO(作為該Function Execution Context的變量對象VO),并將創建的這個活動對象AO加到作用域鏈的最前端
確定this的值
此時Function Execution Context inner可表示為:
innerEC = { scopeChain: { pointer to innerEC.VO, inner.[[Scopes]] }, VO: { arguments: { length: 0 }, }, this: { ... } }
接著進入Function Execution Context inner的執行階段:遇到打印語句console.log(b);,通過inner.[[Scopes]]訪問到變量b=2
至此,函數inner執行完畢,Function Execution Context inner的作用域鏈及變量對象被銷毀
然后函數outer也執行完畢,Function Execution Context outer的作用域鏈及變量對象被銷毀。
這種情況下,函數執行完畢后該銷毀的都被銷毀了,沒有占用內存,所以這種情況下閉包是不會對性能有占用內存方面的影響的。
3.最常被討論的閉包
栗子1
function fa(){ var n = 666; function fb(){ console.log(n); } return fb; } var getN = fa(); getN();
此處,函數fb引用了自由變量n,fb創建了閉包,并且fb被傳遞到了創造它的環境以外(所在的詞法作用域以外)。
這段代碼的執行情況與上面類似,鑒于篇幅就不一一展開詳細描述了,大家可以自己推一遍;現在主要描述一下不同之處,在fa函數的最后,fa函數將它的內部函數fb返回了,按理說返回之后fa函數就執行完畢了,其作用域鏈和活動對象應該被銷毀,但是閉包fb阻止了這件事的發生:
函數fb定義之后其[[Scopes]]屬性被確定,這個屬性至此之后一直保持不變,直至函數fb被銷毀,可以表示為
fb.[[Scopes]] = { pointer to fa.VO, pointer to globalEC.VO }
函數fa執行完畢后,將其返回值--fb函數賦給了全局變量getN,這樣一來由于getN是全局變量,而全局變量是在Global Execution Context中的,需要等到應用程序退出后 —— 如關閉網頁或瀏覽器 —— 才會被銷毀,那么也就意味著fb函數也要到這時才會被銷毀
fb函數的[[Scopes]]屬性中引用了fa函數的變量(活動)對象,意味著fa函數的變量(活動)對象可能隨時還需要用到,這樣一來fa函數執行完畢之后,只有Function Execution Context fa的作用域鏈會被銷毀,而變量(活動)對象仍然會在內存中
這樣遇到getN()語句時,實際上就是調用fb函數,于是順著fb的作用域鏈找到變量n并打印出來
這里我們分析一下,變量n是閉包fb引用的自由變量,創造這個n這個自由變量的是函數fa,此時fa執行完畢之后,自由變量n仍然可以訪問到(仍然存在),并且在fa函數外也能訪問到(離開fa之后)。這一點也就正對應于這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外
除了將內部函數return這種方式之外,還有其他方式可以使用閉包,這些方式的共同之處是:將內部函數傳遞到創造它的環境以外(所在的詞法作用域以外),之后無論在何處執行這個函數就都會使用閉包。
栗子2
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); } foo();
這個栗子中,是通過函數傳參來將內部函數baz傳遞到它所在的詞法作用域以外的
栗子3
var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // 將baz 賦給全局變量 } foo(); fn(); // 2
這個栗子中,是通過賦值給全局變量fn來將內部函數baz傳遞到它所在的詞法作用域以外的。
在栗子1和栗子3這種情況下呢,閉包使得它自己的變量對象以及包含它的函數的變量對象都存在于內存中,如果濫用就很有可能導致性能問題。所以在不需要閉包后,最好主動解除對閉包的引用,告訴垃圾回收機制將其清除,比如在上面這些例子中進行getN = null;fn = null的操作。
4.經常用但可能并沒有意識到它就是閉包的閉包
栗子1
function wait(msg) { setTimeout( function timer() { console.log( msg ); }, 1000 ); } wait( "Hello, closure!" );
上面的代碼其實可以理解為下面這樣:
function wait(msg) { function timer(){ console.log( msg ); } setTimeout( timer, 1000 ); } wait( "Hello, closure!" );
內部函數timer引用了自由變量msg,timer創建了閉包,然后將timer傳遞給setTimeout(..),也就是將內部函數timer傳遞到了所在的詞法作用域以外。
當wait(..) 執行1000 毫秒后,wait的變量對象并不會消失,timer函數可以訪問變量msg,只有當setTimeout(..)執行完畢后,wait的變量對象才會被銷毀。
栗子2
function bindName(name, selector) { $( selector ).click( function showName() { console.log( "This name is: " + name ); } ); } bindName( "Closure", "#closure" );
上面的代碼其實可以理解為下面這樣:
function bindName(name, selector) { function showName(){ console.log( "This name is: " + name ); } $( selector ).click( showName ); } bindName( "Closure", "#closure" );
內部函數showName引用了自由變量name,showName創建了閉包,然后將showName傳遞給click事件作為回調函數,也就是將內部函數showName傳遞到了所在的詞法作用域以外。
當bindName(..)執行之后,bindName的變量對象并不會消失,每當這個click事件觸發的時候showName函數可以訪問變量name。
5.同一個調用函數創建的閉包共享引用的自由變量
function change() { var num = 10; return{ up:function() { num++; console.log(num); }, down:function(){ num--; console.log(num); } } } var opt = change(); opt.up();//11 opt.up();//12 opt.down();//11 opt.down();//10
opt.up和opt.down共享變量num的引用,它們操作的是同一個變量num,因為調用一次change只會創建并進入一個Function Execution Context change,通過閉包留在內存中的變量對象只有一個。
6.不同調用函數創建的閉包互不影響
function change() { var num = 10; return{ up:function() { num++; console.log(num); }, down:function(){ num--; console.log(num); } } } var opt1 = change(); var opt2 = change(); opt1.up();//11 opt1.up();//12 opt2.down();//9 opt2.down();//8
change函數被調用了兩次,分別賦值給opt1和opt2,此時opt1.up,opt2.up以及opt1.down,opt2.down是互不影響的,因為每調用一次就會創建并進入一個新的Function Execution Context change,也就會有新的變量對象,所以不同調用函數通過閉包留在內存中的變量對象是獨立的,互不影響的。
7.關于上面提到的兩點,有一個談到閉包就被拿出來的例子:
for(var i=1;i<6;i++){ setTimeout(function(){ console.log(i); },i*1000); }
上述例子乍一看會覺得輸出的結果是:每隔1s分別打印出1,2,3,4,5;然而實際上的結果是:每隔1s分別打印出6,6,6,6,6。
那么是為什么會這樣呢?下面就來解析一下(ES6之前沒有let命令,不存在真正的塊級作用域):
變量i此處為全局變量,我們考慮全局變量,那么傳遞給setTimeout(...)的這個匿名函數創建了閉包,因為它引用了變量i;雖然循環中的五個函數是在各次迭代中分別定義的,但是它們引用的是全局變量i,這個i只有一個,所以它們引用的是同一個變量(如果在此處將全局對象想象成一個僅調用了一次的函數的返回值,那么這個現象便可以對應于 ———— 同一個調用函數創建的閉包共享引用的自由變量)
而setTimeout()的回調會在循環結束時才執行,即使每個迭代中執行的是setTimeout(.., 0),而循環結束時全局變量i的值已經變成6了,所以最后輸出的結果是每隔1s分別打印出6,6,6,6,6。
要解決上面這個問題,最簡單的方式當然是ES6中喜人的let命令了,僅需將var改為let即可,for 循環頭部的let 聲明會有一個特殊的行為。這個行為指出變量在循環過程中不止被聲明一次,每次迭代都會聲明。隨后的每個迭代都會使用上一個迭代結束時的值來初始化這個變量。
拋開喜人的ES6,又該怎么解決呢,既然上面的問題是由于共享同一個變量而導致的,那么我想辦法讓它不共享,而是每個函數引用一個不同的變量不就好了。上面提到了 ———— 不同調用函數創建的閉包互不影響,我們就要利用這個來解決這個問題:
for(var i=1;i<6;i++){ waitShow(i); } function waitShow(j){ setTimeout(function(){ console.log(j); },j*1000); }
我們將循環內的代碼改成了一個函數調用語句waitShow(i),而waitShow函數的內容就是之前循環體內的內容;waitShow內部傳遞給setTimeout(...)的這個匿名函數仍然創建了閉包,只不過這次引用的是waitShow的參數j。
現在每迭代一次,便會調用waitShow一次,而我們從上文中已經知道不同調用函數創建的閉包互不影響,所以就可以解決問題了!當然,這還不是你常見的樣子,現在我們稍稍改動一下,就變成非常常見的IIFE形式了:
for(var i=1;i<6;i++){ (function(j){ setTimeout(function(){ console.log(j); },j*1000); })(i) }
balabala說了這么多,其實我們平常寫代碼的時候經常無意識的就創建了閉包,但是創建了我們不一定會去使用閉包,而閉包的“威力”需要通過使用才能看得到。
閉包的應用閉包到底有什么用呢?我覺得總結成一句話就是:
“凍結”閉包的包含函數調用時的變量對象(使其以當前值留在內存中),并只有通過該閉包才能“解凍”(訪問/操作留在內存中的變量對象)
粗看可能不是很能理解,下面我們結合具體的應用場景來理解:
恩。。。首先我們來看一個老朋友,剛剛見過面的老朋友
for(var i=1;i<6;i++){ (function(j){ setTimeout(function(){ console.log(j); },j*1000); })(i) }
在這個栗子中,每個IIFE自調用時,其內部創建的閉包將其當時的變量對象“凍結”了,并且通過將這個閉包作為setTimeout的參數傳遞到IIFE作用域以外;所以第一次循環“凍結”的j的值是1,第二次循環“凍結”的j的值是2......當循環結束后,延遲時間到了后,setTimeout的回調執行(即使用閉包),“解凍”了之前“凍結”的變量j,然后打印出來。
既然提到setTimeout,那再來看看另外一個應用,我們知道在標準的setTimeout是可以向延遲函數傳遞額外的參數的,形式是這樣:setTimeout(function[, delay, param1, param2, ...]),,一旦定時器到期,它們會作為參數傳遞給function。但是萬惡的IE搞事情,在IE9及其之前的版本中是不支持傳遞額外參數的。那有時候我們確實有需要傳參數,怎么辦呢。通常的解決方法有下面這些:
function fullName( givenName ){ let familyName = "Swift"; console.log("The fullName is: " + givenName + " " + familyName); } setTimeout(fullName,1000,"Taylor Alison");
使用一個匿名函數包裹
setTimeout(function(){ fullName("Taylor Alison"); },1000);
使用bind(ES5引入)
setTimeout(fullName.bind(undefined,"Taylor Alison"),1000);
polyfill
使用閉包
function fullName( givenName ){ let familyName = "Swift"; return function(){ console.log("The fullName is: " + givenName + " " + familyName); } } let showFullName = fullName("Taylor Alison"); setTimeout(showFullName,1000);
fullName內的匿名函數創建了閉包,并作為返回值返回,調用fullName()后返回值賦給變量showFullName,此時fullName的變量對象被“凍結”,只能通過showFullName才能“解凍”,定時器到期后,showFullName被調用,通過之前被“凍結”的變量對象訪問到givenName和familyName。
待續(有時間補上)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/95533.html
摘要:對數組函數而言,相當于產生了個閉包。關于對象在閉包中使用對象也會導致一些問題。不過,匿名函數的執行環境具有全局性,因此其對象通常指向。由于聲明函數時與聲明函數時的值是不同的,因此閉包與閉包貌似將會表示各自不同的值。 這幾天看到閉包一章,從工具書到各路大神博客,都各自有著不同的理解,以下我將選擇性的抄(咳咳,當然還是會附上自己理解的)一些大神們對閉包的原理及其使用文章,當作是自己初步理解...
摘要:內存泄露內存泄露概念在計算機科學中,內存泄漏指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。判斷內存泄漏,以字段為準。 本文是 重溫基礎 系列文章的第二十二篇。 今日感受:優化學習方法。 系列目錄: 【復習資料】ES6/ES7/ES8/ES9資料整理(個人整理) 【重溫基礎】1-14篇 【重溫基礎】15.JS對象介紹 【重溫基礎】16.JSON對象介紹 【重溫基礎】1...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。異步編程入門的全稱是前端經典面試題從輸入到頁面加載發生了什么這是一篇開發的科普類文章,涉及到優化等多個方面。 TypeScript 入門教程 從 JavaScript 程序員的角度總結思考,循序漸進的理解 TypeScript。 網絡基礎知識之 HTTP 協議 詳細介紹 HTT...
摘要:這是因為我們訪問了數組中不存在的數組元素它超過了最后一個實際分配到內存的數組元素字節,并且有可能會讀取或者覆寫的位。包含個元素的新數組由和數組元素所組成中的內存使用中使用分配的內存主要指的是內存讀寫。 原文請查閱這里,本文有進行刪減,文后增了些經驗總結。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第三章。 我們將會討論日常使用中另一個被開發...
閱讀 2038·2021-10-08 10:05
閱讀 1881·2021-09-22 15:31
閱讀 3002·2021-09-22 15:13
閱讀 3478·2021-09-09 09:34
閱讀 2072·2021-09-03 10:46
閱讀 3112·2019-08-30 15:56
閱讀 1696·2019-08-30 15:53
閱讀 2350·2019-08-30 15:44