前言:閉包可能是JavaScript中的一個特殊存在,在與后端和移動端同學溝通時他們都不知道閉包這個東西,而閉包也是我在面試過程中遇到很多的問題,在面試官問到我閉包是什么的時候我以前是這樣回答的:閉包就是js中的函數(shù)嵌套函數(shù),內部函數(shù)將外部函數(shù)作用域中的變量保存使其在外部能訪問到。
也就是一句話說閉包:閉包是指有權訪問另一個函數(shù)作用域中的變量的函數(shù)。
我覺得閉包就是js執(zhí)行環(huán)境以及作用域產(chǎn)生的一種特殊的環(huán)境。
最近通過文章《我從來不理解JavaScript閉包,直到有人這樣向我解釋它》再看閉包有了更深的理解,特此記錄一下。
作用域與執(zhí)行上下文
在這之前再看幾個小例子:
a = 2;
var a;
console.log(a)
此時輸出應該為什么?
console.log(a);
var a = 2;
那這個呢?
var a = 2;
function a (){}
console.log(a);
這個輸出呢?
再來一個
var a = 2;
a = function(){}
console.log(a);
這個輸出為什么?
這些問題的正確答案也是你看下去的基礎。最好知道正確答案,或者去查閱知道正確答案。
要理解閉包就得先理解兩個東西 一個是作用域,一個是執(zhí)行上下文;
作用域是什么?可以理解成我們可以有效訪問變量或函數(shù)的區(qū)域。它是一個規(guī)則,規(guī)定了執(zhí)行的程序中變量的存儲和訪問。也可以把它理解成一個“地盤”,我的地盤的東西只有我能使用你的地盤的東西我不能使用。
在javascript中又分全局作用域和局部作用域,這里就只說全局作用域與函數(shù)作用域;
來看代碼:
1: var a = 1;
2: function func(){
3: var b = a+1
4: console.log(a+b); // 3
5: }
6: console.log(a); //1
7: func();
8: console.log(b); //Uncaught ReferenceError: b is not defined
這段代碼有一個全局作用域,上面掛載了變量a,和函數(shù)fun;
還有一個函數(shù)fun的作用域,上面掛在了變量b;
關于這段代碼我們細細品一下:(js引擎執(zhí)行順序)
當代碼在解析執(zhí)行的時候從上到下
- 第1行生成變量a,并賦值為10;
- 第2行到第5行是連在一起的,聲明變量fun,并且是一個函數(shù);(函數(shù)未執(zhí)行時,內部東西先不用管)
- 第6行輸出a;
- 第7行函數(shù)func()執(zhí)行,此時回過頭來看2-5行,第3行函數(shù)作用域fun中聲明一個變量b,并賦值為a+1,此時在函數(shù)fun作用域中尋找a,未果,去函數(shù)外也就是父級作用域中尋找a,找到變量a=10,第4行輸出a+b;函數(shù)執(zhí)行完成。
- 第8行輸出b
其實在這個分析過程就是js的執(zhí)行上下文。
執(zhí)行上下文是當前正在執(zhí)行的“代碼環(huán)境”。執(zhí)行上下文有兩個階段:編譯和執(zhí)行。
編譯-在此階段,JS 引擎獲取所有函數(shù)聲明并將其提升到其作用域的頂部,以便我們稍后可以引用它們并獲取所有變量聲明(使用var關鍵字進行聲明),還會為它們提供默認值:undefined。
執(zhí)行——在這個階段中,它將值賦給之前提升的變量,并執(zhí)行或調用函數(shù)(對象中的方法)。
注意:只有使用var聲明的變量,或者函數(shù)聲明才會被提升,相反,函數(shù)表達式或箭頭函數(shù),let和const聲明的變量,這些都不會被提升。
執(zhí)行全局代碼時,會產(chǎn)生一個執(zhí)行上下文環(huán)境,每次調用函數(shù)都又會產(chǎn)生新的執(zhí)行上下文環(huán)境。當函數(shù)調用完成時,這個上下文環(huán)境以及其中的數(shù)據(jù)都會被消除,因為處于活動狀態(tài)的執(zhí)行上下文環(huán)境只有一個。
了解了概念之后再看我們的執(zhí)行過程
第1行 var a = undefined; a = 1;
第6行 輸出a a = 1
第7行 函數(shù)func執(zhí)行回到2-5行,var b = undefined; b = a+1;在函數(shù)func作用域中查找a沒找到向上級找,在父級作用域也就是創(chuàng)建函數(shù)func的作用域中找到a = 1;此時這種向上查找的過程被稱為作用域鏈 ;函數(shù)執(zhí)行完成后上下文環(huán)境以及其中的變量全都被銷毀掉。處于活動狀態(tài)的執(zhí)行上下文環(huán)境只有一個;
第8行 輸出變量b在當前作用域中查找不到,因此報錯b is not defined;
再從變量變化的角度清晰的分析一下這個代碼執(zhí)行過程:
(1)代碼執(zhí)行之前首先創(chuàng)建全局上下文環(huán)境
a: undefined
func: undefined
this: window
(2)接著執(zhí)行代碼到第5行之前,上下文環(huán)境中的變量都在執(zhí)行過程中被賦值。
a: 1
func: function
this: window
(3)當執(zhí)行到第7行的時候調用函數(shù)func時。生成新的執(zhí)行上下文環(huán)境。
b: undefined
this: window
(4)賦值
b:2,
this.window
當func函數(shù)執(zhí)行完畢之后,調用func函數(shù)生成的func上下文環(huán)境銷毀,里面的變量也被銷毀,也就導致了在外面輸出b變量會報錯。
上面可能有些啰嗦,但我只是想從不同的角度去理解這些概念,不管怎么樣,到這里執(zhí)行上下文與作用域大概就稍微清晰一點了。
閉包
有了作用域以及執(zhí)行上下文概念的輔助,理解閉包會更清晰一點。
javascript函數(shù)返回值類型可以是任何類型,如果沒有返回值默認為undefind;
一. 先看一個返回函數(shù)的函數(shù)例子
1: var num = 1;
2: function creater(){
3: let add = function(a,b){
4: let ret = a+b;
5: return ret;
6: }
7: return add;
8: }
9: let func1 = creater();
10: let result = func1(num,2);
11: console.log(result);
- 第1行,聲明變量
num并賦值為1; - 第2-8行,聲明變量
creater并賦值一個函數(shù),暫時不管它內部構造; - 第9行,全局執(zhí)行上下文中聲明變量
func1,暫時,值為undefined; - 第9行,看到括號
(),此時需要執(zhí)行調用一個函數(shù),在全局作用域中查找找到變量creater,然后調用它。 - 調用函數(shù)
creater,走到第2行,此時創(chuàng)建一個新的creater執(zhí)行上下文,此時的活動對象就是creater所在執(zhí)行上下文的活動對象。 - 3-6行,聲明變量
add,并賦值為一個函數(shù),此時add只在creater的執(zhí)行上下文中。 - 第7行,返回變量
add的內容,js引擎在當前作用域中查找名為add的變量,好的在第3行找到,我們返回add的定義,第4-5行括號之間的內容構成add函數(shù)定義。 - 返回時,
creater執(zhí)行上下文被銷毀,add變量同時不復存在,但是,變量add函數(shù)定義仍然存在,以為他返回并賦值給了func1變量。 - 接著走第10行,在全局執(zhí)行上下文聲明一個變量
result,這里拆解一下,先賦值為undefined。 - 接著,需要執(zhí)行一個函數(shù),名為
func1變量中定義的函數(shù),全局查找,它找到有兩個參數(shù)。 - 查找這兩個參數(shù),第一個參數(shù)為第1步的
num,表示數(shù)字1,第二個數(shù)字是2。 - 現(xiàn)在需要執(zhí)行這個函數(shù),函數(shù)定義在3-5行,這時創(chuàng)建了一個
add函數(shù)執(zhí)行上下文,在add執(zhí)行上下文中創(chuàng)建兩個變量a和b。他們分別被賦值為1和2。 - 第4行,在
add執(zhí)行上下文忠聲明一個名為ret的變量,并將變量a,和變量b相加的內容3賦值給了ret。 - 第5行,
ret變量從add函數(shù)中返回,add函數(shù)執(zhí)行上下文被銷毀,變量a,b,ret不再存在。 - 返回值被分配到第10行的
result變量中。 - 打印輸出
result的值。
整個流程大致走下來需要明白幾點:
- 函數(shù)可以存儲在變量中。
- 函數(shù)定義在程序調用之前是不可見的。
- 函數(shù)的返回值可以使任何類型,包括函數(shù)。
- 每次調用函數(shù)時,都會(臨時)創(chuàng)建一個執(zhí)行上下文,當函數(shù)完成時,執(zhí)行上下文銷毀。
- 函數(shù)在遇到return或者 } 時執(zhí)行完成。
二. 看一個閉包
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
接著走一遍流程:
- 第1-8行,創(chuàng)建一個全局變量
createCounter,并指定函數(shù)定義。 - 第9行,創(chuàng)建一個全局變量,
increment; - 第9行,需要調用名為
createCounter的函數(shù)。 - 回到1-8行,調用執(zhí)行函數(shù)
createCounter,創(chuàng)建一個名為createCounter的新的執(zhí)行上下文。 - 第2行,在
createCounter執(zhí)行上下文中聲明一個變量counter,并賦值為0; - 第3行,在
createCounter執(zhí)行上下文中聲明一個變量myFunction,并定義為一個函數(shù),函數(shù)定義在4-5行。 - 第7行,返回
myFunction函數(shù)定義,銷毀createCounter執(zhí)行上下文,createCounter上下文中聲明的變量也全部銷毀。 - 第10行,在全局上下文聲明一個變量
c1。 - 第10行,在全局查找
increment變量,找到并調用它。它的函數(shù)定義在3-6行。 - 回到3-6行,創(chuàng)建一個名為
myFunction的執(zhí)行上下文。 - 第4行,
counter = counter + 1,在myFunction的執(zhí)行上下文中查找counter變量,沒有找到,在全局執(zhí)行上下文中查找counter變量,也未找到。未找到會怎么樣呢?會報錯counter is not defined。 - 這里會有個疑惑,為什么不去第2行去找這個
counter變量?按照我們的理解createCounter在此時是已經(jīng)銷毀的,所以createCounter執(zhí)行上下文里面的變量也是跟著銷毀掉的,所以訪問不到的。 - 到這里按照之前的分析邏輯似乎卡住了,貌似這段程序是沒法執(zhí)行的,但當你去運行這段程序的時候卻發(fā)現(xiàn)它跑的很歡快。所以,這里是有貓膩的,什么貓膩呢?就是
閉包。 - 我們可以這樣理解閉包。
它是這樣工作的,無論何時聲明新函數(shù)并將其賦值給變量,都要存儲函數(shù)定義和閉包。閉包包含在函數(shù)創(chuàng)建時作用域中的所有變量,它類似于背包。函數(shù)定義附帶一個小背包,它的包中存儲了函數(shù)定義創(chuàng)建時作用域中的所有變量。當函數(shù)中返回一個函數(shù)的時候此時不僅僅返回的函數(shù)的定義,還連帶他的背包也一并返回了,而這個背包中就存儲了當前執(zhí)行上下文的所有定義的變量。 - 接下來再回到第7步,第7行返回
myFunction函數(shù)定義,銷毀createCounter執(zhí)行上下文,createCounter上下文中聲明的變量也全部銷毀。但是此時不僅返回了myFunction函數(shù)定義,還一并返回了它的背包。背包里存儲了當前執(zhí)行上下文的所有變量。 - 接下來直接到第11步,第4行,
counter = counter + 1,在myFunction執(zhí)行上下文和全局執(zhí)行上下文之前,先檢查一下背包,背包里含有一個名為counter的變量其值為0,在第4行表達之后它的值被設置為1,并且再次被存儲在背包里;背包現(xiàn)在包含值為1的counter。 - 第5行,
counter值被返回出來,myFunction執(zhí)行上下文被銷毀。 - 回到第10行。返回值1被賦給變量
c1。 - 第11行,重復第9-18步,這一次在背包中變量
counter的值是1,它是在第16步被設置,被遞增為2并存儲在當前執(zhí)行上下文的函數(shù)背包里,c2被賦值為2。 - 第12行,重復第9-18步,
c3被賦值為3。 - 第13行,打印輸出變量
c1,c2,c3的值,分別為1,2,3。
我們上面所說的背包也就是閉包。理解的話就把它理解成函數(shù)默認存在的一種機制,是否所有的函數(shù)都會有閉包,是的。全局范圍創(chuàng)建的函數(shù)也具有閉包,但是由于全局環(huán)境變量都會訪問到,所以全局范圍的函數(shù)的閉包就顯得不那么重要。但是當函數(shù)作為返回值被另外函數(shù)返回的時候,閉包就顯得尤為重要,因為,被返回的函數(shù)可以訪問到不屬于全局作用域的變量,就是閉包中的變量。
最后總結一下,函數(shù)在被當做返回值返回的時候,該函數(shù)會攜帶自己的閉包,閉包是該函數(shù)聲明時的作用域內部所有變量。
我們已經(jīng)知道上面代碼輸出為1,2,3;
那么看一下下面這段代碼c1,c2,c3分別輸出多少呢?
function createCounter() {
let counter = 0
const myFunction = function(counter) {
counter = counter + 1
return counter
}
return myFunction(counter)
}
const increment = createCounter()
const c1 = increment
const c2 = increment
const c3 = increment
console.log('example increment', c1, c2, c3)
這是一種避免閉包的方式,閉包容易導致內存泄漏,造成性能問題。因此在寫程序的時候盡量避免使用閉包。
這樣將變量以參數(shù)的形式傳遞給被返回函數(shù),這樣就可以避免閉包。
覺得對你有用的,點個贊吧?。?!