前言
對于js中的閉包,無論是老司機還是小白,我想,見得不能再多了,然而有時三言兩語卻很難說得明白,反正在我初學時是這樣的,腦子里雖有概念,但是卻道不出個所以然來,在面試中經常會被用來吊自己的胃口,考察基礎,雖然網上自己也看過不少相關閉包的文章,帖子,但貌似這玩意,越看越復雜,滿滿逼格高,生涉難懂的專業詞匯常常把自己帶到溝里去了,越看越迷糊,其實終歸結底,用楊絳先生的一句話就是:“你的問題在于代碼寫得太少,書讀得不夠多",其實在我看來前者是主要的,是后者的檢驗, 自知目標搬磚20年(還差19年..),其實,閉包的應用,在我們不經意間就使用了,是無處不在的,盡管知道閉包是個麻煩的玩意,自己也經常吃回頭草看看這家伙的,所謂出去混,遲早是要還的,今天,我就對閉包的一點理解作一點點總結,關于閉包,我也一直在學習當中...話說多了,都是故事,直接擼起袖子,開始干吧
正文從這里開始~
理解上下文和作用域
其實上下文與作用域是兩個不同的概念,有時我自己也經常混淆,把它們視為是同一個東西,我們知道函數的每次調用都會有與之緊密相連的作用域和上下文,從本質上說,作用域其實是基于函數的
,而上下文
是基于對象的
,也就是說作用域是涉及到它所被調用函數中的變量訪問,而調用方法和訪問屬性又存在著不同的調用場景(4種調用場景,函數調用,方法調用,構造器函數調用,call(),apply()間接調用),而上下文始終是this
所代表的值,它是擁有控制當前執行代碼的對象的引用
變量作用域
在javascript中,作用域是執行代碼的上下文(方法調用中this所代表的值),作用域有三種類型:全局作用域(Global scope
),局部作用域(Local/Function scope
,函數作用域)和eval作用域,在函數內部使用var
定義的代碼,其作用域都是局部的,且只對該函數的其他表達式是可見的
,包括嵌套子函數中的代碼,局部變量只能在它被調用的作用域范圍內進行讀和寫的操作,在全局作用域內定義的變量從任何地方都是可以訪問的,因為它是作用域鏈中的最高層中的最后一個,在整個范圍內都是可見的,注意在Es6之前是沒有塊級作用域的,而Es6后是有的,也就是說Esif
,while
,switch
,for
語句是有了塊級作用域的,可以使用let
關鍵字聲明變量,修正了var
關鍵字的缺點,注意let使用規則
看如下代碼所示:
* 全局變量與局部變量
*
* @global variable {variable="itclab"}
* @function myFun
* @local variable {variable="itclanCode",variable=24}
* @function otherFun
* @eval作用域 evalfun
*/
var variable = "itclan"; //全局變量
console.log("全局variable","=",variable); // 全局variable = itclan
// 函數表達式
var myFun = function(){
var variable = "itclanCode"; //局部變量
console.log("局部variable","=",variable); // 局部variable = itclanCode
var otherFun = function(){
var variable = 24; //局部變量
console.log("局部variable","=",variable); // 局部variable = 24
}
otherFun();
}
myFun();
eval("var evalfun = 20;console.log('evalfun作用域','=',evalfun)");// evalfun作用域 = 20
注意
- 函數可以嵌套函數,并可以無限的嵌套下去,也就是可以創建無數的函數作用域和eval作用域,而
javascript
壞境只是用一個全局作用域 - 全局作用域(
global scope
)是作用域鏈中的最后一層 - 包含函數的函數,會創建堆棧執行的作用域,這些鏈接在一起的棧通常被稱為作用域鏈(也就是后面會提到閉包產生的本質原因)
什么是執行壞境
所謂執行壞境,它定義了變量或函數有訪問的其他數據的能力,它決定了各自的行為,它的側重點在于函數的作用域,而并不是所要糾結的上下文,一旦函數一聲明定義,就會自動的分配產生了作用域,有著自己的執行壞境,執行壞境可以分為創建與執行兩個階段,在創建階段,js解析器首先會創建一個變量對象(活動對象),它由定義在執行壞境中的變量,函數聲明和參數組成,在這個階段,系統會自動的產生一個this
對象,作用域鏈會被初始化,隨之,this
的值也會被確定,第二階段,也就是代碼執行,代碼會被解釋執行,你會發現,每個執行壞境都有一個與之關聯的變量對象,執行壞境中所有定義的變量和函數都保存在這個對象中,注意,我們是無法手動的訪問這個對象的,只有js解析器才能夠訪問它,其實也就是this,盡管很抽象,但是理解它還是蠻重要的
作用域鏈(詞法作用域)
當javascript
查找與變量相關聯的值時,會遵循一定的規則,也就是沿著作用域鏈從當前函數作用域內逐級的向上查找,直到頂層全局作用域結束,若找到則返回該值,若無則返回undefined,這個鏈條是基于作用域的層次結構的,一旦當代碼在壞境中執行時,會自動的創建一個變量對象的作用域鏈,其作用域鏈的用途也就是保證對執行壞境的全局變量和具有訪問權限函數內的局部變量定制特殊的規則,由內到外有序的對變量或者函數進行訪問,作用域鏈包含了在壞境棧中的每個執行壞境對應的變量對象,通過作用域鏈可以決定變量的訪問與標識符的解析,如下代碼所示:
* 作用域鏈變量的訪問
*
* @global variable {name="隨筆川跡"}
* @function fun1,fun2
* @local variable {oTherName="哇嘎嘎",AliasName = "川川"}
* @return {fun2,name,oTherName,AliasName}
* @return fun2,fun1函數的返回結果值為fun2的值
*
*
*/
var name = "隨筆川跡"; // 全局變量
var fun1 = function(){
var oTherName = "哇嘎嘎"; // 局部變量
var fun2 = function(){
var AliasName = "川川"; // 局部變量
AliasName = oTherName;
oTherName = AliasName;
return {name,oTherName,AliasName};
}
console.log(fun2());
return fun2();
}
//console.log(fun2()); // 若在全局作用域調用訪問fun()會失敗,顯示fun2 is not defined
console.log(fun1(),"name is","=",name)
當我們分析這段代碼時,首先全局范圍全局變量name
,函數fun1嵌套fun2函數,fun1,fun2函數內局部變量分別為:oTherName
,AliasName
當在函數fun2內,并未聲明name變量
便在該函數fun2內進行了訪問,這是如何找到的?javascript首先在當前fun2函數作用域內查找一個名為name的變量,但是在fun2并未找到,于是它會查找它的父函數fun1的作用域內進行查找,但是發現仍然沒有找到,于是在往外進行查找,結果在全局作用域范圍內查找了name的值
于是找到了便把該值進行返回,若是在全局作用域內還未找到則會返回undefined,注意在函數fun2作用域內,name,oTherName,AliasName都是可以訪問的,而在函數fun1函數作用域內是訪問不了oTherName
的,因為它脫離了fun1
的函數的作用域嘛,我們知道在函數外是無法訪問函數里面的的變量的,訪問變量由內向外進行查找是可以的,但是反之則就不行,從上圖的箭頭分析圖可知,內部壞境中,是可以通過作用域鏈訪問它所有的外部壞境
但是在外部壞境是無法訪問內部壞境中的任何變量和函數,這點很重要,我們在函數嵌套函數,并且進行函數調用時,要格外注意,如果在編程當中出現這種函數is not defined
那么就是牽扯到函數作用域的問題了,在函數外是無法訪問函數內的變量或者函數的,當然這種問題是可以解決的,也就是后面提到的閉包
其實上面我們的代碼中就已經無形用了閉包,匿名函數fun1,fun2就是個閉包,嵌套函數與被嵌套壞境的連接是線性的,有次序的,對于標識符(也就是變量或者函數名查找)是從當前函數作用域開始,沿著作用域鏈逐級的向上查找,直到最頂端全局變量壞境,若找到該值則返回,若無則返回undefined
注意
:理解作用域以及作用域鏈對理解原型鏈是很有幫助的,其實他們區別并不是很大,兩者都是通過位置體系(上下嵌套關系)和分層體系來查找值的方法,進而可以對變量或者函數進行讀和寫的操作,如下代碼所示:
var x = 5;
var fun1 = function(){
var y = 10;
var fun2 = function(){
var z = 20;
return z+y+x;
}
fun2();
return fun2();
}
console.log("x+y+z的和=",fun1()); //x+y+z的和= 35
javascript沒有塊級作用域
在Es6之前,如if,for,while,switch邏輯語句是無法創建作用域,也就是它后面的雙大括號并沒有域的作用,這才得式變量可以相互覆蓋,解決辦法,你可以使用es6的let關鍵字聲明變量,注意let的使用,如下代碼所示
var str = "itclan"; // 全局變量
console.log(str); // itclan
if(true){ // if邏輯語句
str = "itclanCode";
console.log(str); // itclanCode
for(var i = 0;i<=2;i++){
str = i;
console.log(str); // 0,1,2
}
console.log(str); // 2
}
console.log(str); // 2
因此,代碼在執行過程中,從上到下,str是變化的,因為在Es6之前,沒有塊級作用域,只有全局作用域,函數作用域,eval()作用域
注意
在函數中應用var
聲明變量,避免作用域的陷阱
javascript會將缺少var
的變量聲明,即便在函數或者封裝在函數中,都會視為全局變量作用域,而非局部作用域,我們是不應該出現這種不要var聲明的,這樣會造成全局變量的污染,易混淆,如下代碼所示
* 如果不使用var來聲明變量,那么,該變量實際上是在全局作用域中定義,而不是局部作用域中定義(它本是在局部作用域中定義)
*
* @descortion:這樣很容易產生誤解,應當杜絕這么干
* @在函數內定義的變量應用var,當然要在函數內部創建或更改全局作用域內的屬性就另當別論了的
*
*
*/
var fun1Exp = function(){
var fun2Exp = function(){
name = "污葵"; // 沒有使用var,它相相當于window.name
}
fun2Exp();
}
fun1Exp();
console.log({name});
// 相反,使用var的情況
var fun3Exp = function(){
var fun4Exp = function(){
var age = 20; //使用var,局部變量
}
fun4Exp();
}
fun3Exp();
console.log(age); //Uncaught ReferenceError: age is not defined,報錯的原因,age在fun4Exp函數作用域中,在函數外是訪問不了函數內部的變量的
作用域是在函數定義時就確定的,而非調用時確定
因為函數決定作用域,又因為函數也是對象,也是一種數據類型,一樣可以像基本數據類型值一樣被作為值來傳遞,作用域就是根據函數定義時的位置確定的,而與該函數在哪里被調用無關,其實就是詞法作用域,作用域鏈是在調用函數之前創建,也是這樣,我們就可以創建閉包,我們常常是這么做的,讓函數向全局作用域返回一個嵌套函數,但該函數仍然能夠通過作用域訪問它父函數的作用域,作用域鏈是在定義時確定的,并在函數內部傳遞代碼不會改變作用域
如下代碼所示:
* 作用域鏈是在函數定義時位置確定的,而非函數調用位置,在函數內部傳遞代碼不會改變作用域鏈
*
* @funtion expression parentFun
* @local variable localVal
* @return parentFun的返回值為一個匿名函數,訪問該匿名函數外的變量
*
*/
var parentFun = function(){
var localVar = "itclan是個有溫度的公眾號";
return function(){ // 返回一個匿名函數
console.log(localVar);
}
}
var nestedFun = parentFun();//nestedFun引用parentFun函數,把函數parentFun函數的返回值賦值給變量nestedFun
nestedFun(); // 輸出itclan是個有溫度的公眾號,因為返回的函數可以通過作用域鏈訪問到localVar變量
產生閉包的根本原因是作用域鏈
在通過上面的了解變量的作用域和作用域鏈后,相信你理解閉包就不難了,如下代碼所示:
* 閉包是由作用域鏈引用的
*
* @function expression countNum 匿名函數
* @local variable count
* @return 匿名函數
*
*/
var countNum = function(){
var count = 0;
return function(){ //調用countNum的時候返回嵌套的子函數
return ++count;// count在作用域鏈內定義,父函數里
};
}(); // 匿名函數的立即調用,返回嵌套函數
// countNum(),上面的匿名函數后若不加括號調用,則返回的結果將是return 后面的函數的整體代碼
console.log(countNum()); // 1
console.log(countNum()); // 2
console.log(countNum()); // 3
當每次調用countNum函數時,嵌套在該函數內的匿名函數是可以訪問父函數(這里指的是countNum的)作用域的,其實這就是所謂的閉包,作用鏈就是閉包的橋梁,用來連接內部函數與外部函數的關系,從而達到外部函數訪問內部函數局部變量或者函數的目的,其中被嵌套函數就可以稱為是一個閉包
小結
- 產生閉包的原因是由作用域鏈引起的
- 函數嵌套函數,被嵌套的函數就可以稱為閉包
- 子函數可以使用父函數的變量(訪問其他函數內部的局部變量)
- 讓變量始終保存在內存中,避免自動垃圾回收(其實上面的例子中就已經用到了的)
- 對外提供公有屬性和方法
總結:
整篇文章從理解上文和作用域開始,以及什么是執行壞境,其產生閉包的原因是作用域鏈,并知道在Es6之前是沒有塊級作用域的概念的,并且作用域是在函數定義時就確定的,而非函數調用確定,在我的理解中編程其實很大一部分就是對數據進行讀和寫的操作
其中讀可以理解對定義變量數據的訪問,而寫可以理解賦值,引用,變更,改寫操作,當然js中不像其他后臺語言的存儲數據類型那般復雜,基本就是基本數據類型和對象了
理解作用域以及作用域鏈對理解閉包是相當的重要,對后續的原型鏈以及繼承都是相關聯的,其實也不必抓著什么執行壞境和上下文這些相對抽象的概念不放,我們只有在平時的使用當中,稍稍留意就行,在應用中結合理論進行驗證,當然閉包的內容遠不及此..
以下是本篇提點概要
- 理解上下文和作用域,作用域是基于函數的,而上下文是基于對象的,雖然說函數也是對象,但是這里更多的是指對象直接量的表示法,上下文始終圍繞著this所代表的值,它是擁有控制當前執行代碼對象的引用
- 變量的作用域,在Es6之前沒有塊級作用域,而Es6有了塊級作用域,也就是if,,while,switch,for,若使用let關鍵字,則具備塊級作用域,也就是說定義在雙大括號內的變量,在雙大括號內的才起作用,一旦離開該范圍,就不起作用了
- 什么是執行壞境,定義了變量或函數有訪問的其他數據的能力,它決定了各自的行為,它的側重點在于函數的作用域,而并不是所要糾結的上下文,分為創建壞境和執行壞境
- 作用域鏈(詞法作用域),當查找與變量相關聯的值時,會遵循一定的規則,也就是沿著作用域鏈從當前函數作用域內逐級的向上查找,直到頂層全局作用域結束,若找到則返回該值,若無則返回undefined
- javascript沒有塊級作用域,往往很多時候使用匿名函數自執行來模擬塊級作用域
- 作用域是在函數定義時就確定的,而非調用時確定,作用域就是根據函數定義時的位置確定的,而與該函數在哪里被調用無關,其實就是詞法作用域
- 產生閉包的根本原因是作用域鏈,見上小結