閉包(closure
)是JavaScript
中一個「神祕」的概念,許多人都對它難以理解,我也一直處於似懂非懂的狀態,前幾天深刻了解了一下執行環境以及做用域鏈,可戳查看詳情,而閉包與做用域及做用域鏈的關係密不可分,因此就再深刻去理解了一番。前端
Lexical Scope
首先咱們來理解一下做用域的概念:編程
一般來講,一段程序代碼中所用到的標識符並不老是有效/可用的,而限定這個標識符的可用性的代碼範圍就是這個標識符的做用域
做用域有詞法做用域與動態做用域之分,詞法做用域也可稱爲靜態做用域,這樣與動態做用域看起來更對應。segmentfault
JavaScript
下面咱們使用代碼來講明二者的區別(此處僅僅使用JavaScript
來講明兩種狀況,實際上JavaScript
只基於詞法做用域)數組
var cc = 6; function foo() { console.log(cc); // 會輸出6仍是66? } function bar() { var cc = 66; foo(); } bar();
foo
函數的時候就肯定了,foo
函數的內部要訪問變量cc
,因爲foo
的內部做用域中沒有cc
變量,因此會根據做用域鏈訪問到全局中的cc
變量;這與在何處調用foo
函數無關。foo
函數在何處被調用,而不關心它定義在哪裏;foo
函數的內部要訪問變量cc
,而foo
的內部做用域中沒有cc
變量時,會順着調用棧在調用 foo()
的地方查找變量cc
,此處是在bar
函數中調用的,因此引擎會在bar
的內部做用域中查找cc
變量,這個cc
變量的值爲66Lexical Scope Chain
var 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--上卷》中:性能
當函數能夠記住並訪問所在的詞法做用域時,就產生了閉包,即便函數是在當前詞法做用
域以外執行
在維基百科的定義:this
在計算機科學中,閉包(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
。