作者:
極客小俊
公眾號: 同名
從來都沒有理解JavaScript閉包? 今天非把你教會(huì)不可! 看這一篇就夠了,全程大白話!
前言
這么多年了哲银,你是否還在討論javascript閉包
呢? 閉包
這個(gè)概念幾乎也是任何前端面試官都會(huì)必考的問題!
并且理解javascript閉包
也是邁向高級前端開發(fā)工程師
的必經(jīng)之路!
也只有理解了閉包
的原理和運(yùn)行
機(jī)制才能寫出更為安全和優(yōu)雅的javascript
代碼
那么你是否學(xué)習(xí)javascript
很久了但閉包
還沒有搞懂呢? ?? 閉包
很晦澀難懂嗎? 或許你把閉包
這個(gè)概念想象得太過神奇! 今天就來揭秘javascript閉包
一個(gè)前端開發(fā)經(jīng)久不衰的話題!
學(xué)習(xí)條件
這里我也特別說明一下閉包
其實(shí)牽扯的東西還是有點(diǎn)多,涉及到以下JS知識點(diǎn)
:
函數(shù)的執(zhí)行上下文環(huán)境(Execution context of function)
變量對象(Variable object)
活動(dòng)對象(Active object)
作用域(scope)
作用域鏈(scope chain)
那么如果你對以上所涉及到的知識點(diǎn)
還沒有清楚,那么建議補(bǔ)一下析校,我以后也會(huì)慢慢提及, 否則理解閉包
就會(huì)出現(xiàn)歧義!
到底什么是閉包?
概述
閉包
比較書面化的解釋是: 一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式,并且通常是一個(gè)函數(shù), 而這些變量也是該表達(dá)式的一部分
礁哄。我想如果你是一個(gè)零基礎(chǔ)
的小白, 那么估計(jì)不出意外的話應(yīng)該完全不能理解這句話!?? 沒關(guān)系想搞懂我們接著往下看...
那么我們首先來看一段JS
代碼
//函數(shù)定義
function outerTest() {
var num = 0;
function innerTest() {
++num
console.log(num);
}
return innerTest;
}
//調(diào)用
var fn1 = outerTest();
fn1();
fn1();
fn1();
運(yùn)行結(jié)果
以上就是一個(gè)閉包
的經(jīng)典案例, 我們慢慢來分析!
其實(shí)你會(huì)發(fā)現(xiàn)以上這段JS
代碼有兩個(gè)特點(diǎn):
1长酗、innerTest函數(shù)
嵌套在outerTest函數(shù)
的內(nèi)部
2、outerTest函數(shù)
的返回值就是innerTest函數(shù)
那么有人就會(huì)說函數(shù)嵌套函數(shù)
就是閉包
其實(shí)這樣子說是不嚴(yán)謹(jǐn)?shù)?
原理分析
接著之前的那一段JS代碼
我們來看一張圖
代碼分析
當(dāng)在執(zhí)行完var fn1 = outerTest();
之后桐绒,變量fn1
實(shí)際上是指向了函數(shù)innerTest
夺脾,
那么接下來如果再執(zhí)行fn1()
就會(huì)改變num
變量的值, 當(dāng)然這個(gè)過程通常懂一點(diǎn)程序執(zhí)行流程也可以分析出來!
關(guān)鍵不同的是之后繼續(xù)執(zhí)行fn1()
輸出的卻是num變量
累加之后的結(jié)果! 你肯定想知道為什么會(huì)累加!對吧!??
首先因?yàn)?code>函數(shù)innerTest引用了函數(shù)outerTest
內(nèi)部的變量或者數(shù)據(jù),再然后重點(diǎn)來了:
如果實(shí)在你還無法理解這里的【作用域鏈】咧叭,那么你可以理解為是一種描述路徑的術(shù)語, 沿著該路徑可以找到需要的變量值!
再次回到閉包
的概念上來, 也就是當(dāng)一個(gè)子函數(shù)
引用了父級函數(shù)的某個(gè)變量或數(shù)據(jù)
蚀乔,那么 閉包
其實(shí)就產(chǎn)生了
并且這個(gè)變量或數(shù)據(jù)
的生命周期始終能保持使用,就能間接保持原構(gòu)父級函數(shù) 在內(nèi)存中的變量對象
不會(huì)消失
所以盡管outerTest()函數(shù)
已經(jīng)調(diào)用結(jié)束, 但是子函數(shù)
卻始終能引用到該父級函數(shù)
中的變量的值菲茬,并且該變量值只能通這種方法來訪問!
即使再次調(diào)用相同的outerTest()函數(shù)
吉挣,但只會(huì)生成相對應(yīng)的變量對象
,新的變量對象
只是對應(yīng)新的值, 和上次那次調(diào)用的是各自獨(dú)立的!
如圖
簡而言之 在嵌套在父級函數(shù)
內(nèi)部的子函數(shù)被定義時(shí),并且也引用了父級函數(shù)的數(shù)據(jù)時(shí)
就產(chǎn)生了閉包
需要重點(diǎn)注意的是: 一個(gè)閉包內(nèi)對變量的修改婉弹,不會(huì)影響到另外一個(gè)閉包中的變量
以上案例就是在outerTest函數(shù)
執(zhí)行完并返回后睬魂,閉包
使得JS
中的的垃圾回收機(jī)制GC(Garbage collection)
不會(huì)收回outerTest函數(shù)
所占用的資源,這里指的資源是它的變量對象
, 因?yàn)?code>outerTest函數(shù)的內(nèi)部函數(shù)innerTest
的執(zhí)行一直需要依賴outerTest函數(shù)
中的變量或者其他數(shù)據(jù)镀赌。這就是對閉包
產(chǎn)生和特性最直白通俗的描述!
那么現(xiàn)在回過頭來再次理解為什么每次調(diào)用fn1()函數(shù)
變量num
會(huì)累加? 看下面這張圖!
如圖
因?yàn)橛捎?code>閉包的存在使得函數(shù)outerTest
返回后氯哮,函數(shù)outerTest
中的num變量
其實(shí)始終存在與內(nèi)存中,這樣每次執(zhí)行fn1()
佩脊,都會(huì)找到內(nèi)存中與之對應(yīng)outerTest函數(shù)
的變量對象
的num變量
進(jìn)行累加1后,輸出num
的值
閉包具體步驟總結(jié)
- 當(dāng)執(zhí)行
函數(shù)outerTest
的時(shí)候蛙粘,outerTest函數(shù)
會(huì)進(jìn)入相應(yīng)的執(zhí)行上下文環(huán)境
! - 在創(chuàng)建
函數(shù)outerTest
執(zhí)行環(huán)境的過程中,首先會(huì)為函數(shù)outerTest
添加一個(gè)scope屬性
威彰,即函數(shù)outerTest
的作用域出牧,其值就為函數(shù)outerTest
中的作用域鏈scope chain
- 然后
執(zhí)行環(huán)境
會(huì)創(chuàng)建一個(gè)活動(dòng)對象(activation object)
⌒危活動(dòng)對象也是當(dāng)前被調(diào)用這個(gè)函數(shù)所擁有的一個(gè)對象舔痕,它是用來保存數(shù)據(jù)的, 它不能通過JS
代碼直接訪問, (如果你實(shí)在理解不了可以想象成一個(gè)抽象的對象) - 創(chuàng)建完
活動(dòng)對象
后,把該活動(dòng)對象
添加到outerTest函數(shù)
的作用域鏈
中的最頂端,也就是圖中的第0位
,此時(shí)outerTest函數(shù)
的作用域鏈
包含了兩個(gè)對象:outerTest函數(shù)
的活動(dòng)對象
和全局window變量對象
也就是圖中藍(lán)色和綠色
兩個(gè)對象 - 然后在
outerTest函數(shù)
的活動(dòng)對象
上添加一個(gè)arguments屬性
豹缀,它保存著調(diào)用outerTest函數(shù)
時(shí)所傳遞的實(shí)際參數(shù)伯复,當(dāng)然我們這里并沒有傳遞任何參數(shù)進(jìn)來! - 再然后把所有
outerTest函數(shù)
的形參
和內(nèi)部的innerTest函數(shù)
、以及num變量
這些數(shù)據(jù)的引用也添加到outerTest函數(shù)
的活動(dòng)對象
上邢笙。 - 此時(shí)完成了
函數(shù)innerTest
的定義啸如,因此如同第3步,函數(shù)innerTest
的作用域鏈
以及innerTest函數(shù)
的變量對象跟之前outerTest函數(shù)
一樣被初始化了, 那么到這里整個(gè)outerTest函數(shù)
從定義到執(zhí)行的步驟就完成了! - 然后在外部
outerTest函數(shù)
返回innerTest函數(shù)
命名為fn1
的引用變量
氮惯,又因?yàn)?code>innerTest函數(shù)的作用域鏈
包含了對outerTest函數(shù)
的變量對象
的引用叮雳,注意:此時(shí)outerTest函數(shù)已經(jīng)調(diào)用結(jié)束,活動(dòng)對象也變成了內(nèi)存中滯留的變量對象
,那么innerTest函數(shù)
可以訪問到outerTest函數(shù)
中定義的所有變量和函數(shù)
, 并且innerTest函數(shù)
被外部的fn1
所引用,函數(shù)innerTest
又依賴函數(shù)outerTest
妇汗,因此函數(shù)outerTest
的變量對象
在返回后不會(huì)被JS垃圾回收機(jī)制GC(Garbage collection)
銷毀帘不。
所以當(dāng)fn1
執(zhí)行也相當(dāng)于在執(zhí)行函數(shù)innerTest
時(shí)候也會(huì)像以上步驟一樣。因此執(zhí)行時(shí)innerTest函數(shù)
的作用域鏈
包中含了3個(gè)對象:innerTest函數(shù)
的活動(dòng)對象
杨箭、outerTest函數(shù)
的變量對象
和全局window變量對象
, 也就是圖中藍(lán)色+綠色+紫色
三個(gè)對象, 如果你覺得上圖看不清楚那么就看下面這張圖!
如圖
當(dāng)在innerTest函數(shù)
中訪問一個(gè)變量
時(shí),搜索順序是先搜索自身的活動(dòng)對象
如果存在則返回
注意: 如果函數(shù)innerTest存在prototype原型對象寞焙,則在查找完自身的活動(dòng)對象后, 會(huì)先查找自身的原型對象
如果不存在將繼續(xù)搜索滯留在內(nèi)存中outerTest函數(shù)
的變量對象
,依次查找直到找到為止, 這就是JS
中的數(shù)據(jù)查找機(jī)制 ,當(dāng)然如果整個(gè)作用域鏈上都無法找到,則返回undefined
我們在理解閉包的時(shí)候 重點(diǎn)也是在作用域鏈這個(gè)環(huán)節(jié)容易出錯(cuò), 要知道函數(shù)的定義與執(zhí)行的區(qū)別捣郊。
函數(shù)
的作用域
是在函數(shù)定義時(shí)
就已經(jīng)確定辽狈,而不是在執(zhí)行的時(shí)候確定, 這里引出了一個(gè)概念詞法作用域
舉個(gè)栗子??
function outer(num) {
function inner() {
return num;
}
return inner;
}
var fn1 = outer(1);
console.log(fn1());
我們假設(shè)函數(shù)fn1
的作用域
是在執(zhí)行時(shí)
,也就是console.log(fn1())
確定的,那么此時(shí)fn1
的作用域鏈?zhǔn)侨缦拢?/p>
函數(shù)fn1的活動(dòng)對象->console.log的活動(dòng)對象->window對象
模她,如果假設(shè)成立稻艰,那么輸出值就必然是undefined
另一種假設(shè)也就是函數(shù)fn1
的作用域是在定義時(shí)
確定的懂牧,就是說fn1
指向的inner函數(shù)
在定義的時(shí)候就已經(jīng)確定了作用域
侈净。那么在執(zhí)行的時(shí)候,函數(shù)fn1
的作用域鏈為如下:
函數(shù)fn1的活動(dòng)對象->函數(shù)outer的變量對象->window對象
僧凤,如果假設(shè)成立畜侦,那么輸出值也就是1。
所以運(yùn)行結(jié)果最終為1躯保,說明了第2種假設(shè)是正確的旋膳,也就證明了函數(shù)的作用域確實(shí)是在定義這個(gè)函數(shù)的時(shí)候就已經(jīng)確定了
這個(gè)說法!
有人又會(huì)問如果我們不返回outerTest函數(shù)
行不行呢? 答案肯定是不行的
因?yàn)?code>outerTest函數(shù)執(zhí)行完后,innerTest函數(shù)
沒有被返回給外界途事,只是被outerTest函數(shù)
所使用
因此函數(shù)outerTest
和函數(shù)innerTest
互相使用, 但又不被外界使用验懊,那么函數(shù)outerTest
執(zhí)行完畢之后就會(huì)被GC(Garbage collection)
垃圾回收機(jī)制回收, 那么outerTest函數(shù)
的執(zhí)行上下文環(huán)境
也會(huì)被彈出call Stack
, 內(nèi)存中也不會(huì)在有outerTest函數(shù)
所對應(yīng)的變量對象
了, 自然也無法繼續(xù)保存值了!
閉包的應(yīng)用場景
應(yīng)用場景1 代碼模塊化
閉包的應(yīng)用場景主要是用于模塊化
閉包
可以一定程度上保護(hù)函數(shù)內(nèi)的變量
安全。
還是剛才的案例舉例!
outerTest函數(shù)
中的num變量
只有innerTest函數(shù)
才能訪問尸变,而無法通過其他途徑訪問到义图,因此保護(hù)了num變量
的安全性, 所以閉包模塊化
基本可以解決函數(shù)污染
或變量
隨意被修改問題!
比如說Java、php
等語言中有支持將方法聲明為私有召烂,它們只能被同一個(gè)類中的其它方法所調(diào)用碱工。
而 js
是沒有這種原生支持的,但我們可以使用閉包
來模擬私有方法
奏夫。
私有方法
不僅僅有利于限制對代碼的訪問權(quán)限, 還提供了管理全局命名空間的強(qiáng)大能力怕篷,避免非核心的方法弄亂了代碼的公共接口部分。
舉個(gè)栗子??
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* 輸出 0 */ Counter.increment(); //執(zhí)行遞增 Counter.increment(); //執(zhí)行遞增 console.log(Counter.value()); /* 輸出 2 */ Counter.decrement(); //執(zhí)行遞減 console.log(Counter.value()); /* 輸出 1 */
如圖
以上案例表現(xiàn)了如何使用閉包
來定義公共函數(shù)酗昼,并讓它可以訪問私有函數(shù)
和變量
IIFE匿名函數(shù)
包含兩個(gè)私有
數(shù)據(jù):名為 privateCounter 變量
和 changeBy函數(shù)
, 而這兩項(xiàng)都無法在這個(gè)匿名函數(shù)
外部直接訪問廊谓。必須通過匿名函數(shù)
返回的三個(gè)公共函數(shù)接口
來進(jìn)行訪問!
increment()、decrement()麻削、value()
這三個(gè)公共函數(shù)是共享同一個(gè)作用域執(zhí)行上下文環(huán)境的變量對象
, 也就是閉包也多虧 js
的作用域
蒸痹,它們都可以訪問 privateCounter變量
和 changeBy函數(shù)
應(yīng)用場景2 在內(nèi)存中保持變量數(shù)據(jù)一直不丟失!
還是以最開始的例子, 由于閉包
的影響碟婆,函數(shù)outerTest
中num變量
會(huì)一直存在于內(nèi)存中电抚,因此每次執(zhí)行外部的fn1()
時(shí),都會(huì)給num變量
進(jìn)行累加!
所以每累加一次也就是每調(diào)用一次fn1()
就會(huì)去內(nèi)存中一層層尋找outerTest函數(shù)
的變量對象
里面的num
進(jìn)行累加!
現(xiàn)在完全明白了閉包
了吧!??
如果你真的理解了閉包竖共,那么下面這個(gè)案例就很容易去推理了蝙叛,也非常經(jīng)典 就是在事件循環(huán)中如何保留每一次循環(huán)的索引值!
代碼栗子
html代碼
<button>Button0</button> <button>Button1</button> <button>Button2</button> <button>Button3</button> <button>Button4</button>
js代碼
window.onload=function(){ var btns = document.getElementsByTagName('button'); for(var i = 0,len = btns.length; i < len; i++) { btns[i].onclick = function() { console.log(i); } } }
分析
通過執(zhí)行該段代碼,其實(shí)你會(huì)發(fā)現(xiàn)不論點(diǎn)擊哪個(gè)button按鈕
公给,均輸出5
,
如圖
這是很多初學(xué)者 或者還沒有完全理解閉包的朋友心中的困惑! ?? 那今天就要跟你解開這個(gè)困惑了!
首先你要明白一點(diǎn), onclick事件
是被異步觸發(fā)的借帘,也就是等著用戶事件被觸發(fā)時(shí)蜘渣,for循環(huán)
其實(shí)早已結(jié)束!
此時(shí)變量 i
的值已經(jīng)是5
所以當(dāng)onlick事件函數(shù)
順著作用域鏈
從內(nèi)向外查找變量 i
時(shí),找到的值總是 5
也就是這個(gè)變量i
已經(jīng)在外層的變量對象
中一直保存的都是最終值!
如果你想要每次都打印出所 對應(yīng)的索引號
這里就要使用到閉包
了!
修改js代碼如下形式
window.onload=function(){
var btns = document.getElementsByTagName('button');
for(var i = 0, len = btns.length; i < len; i++) {
(function(i) {
btns[i].onclick = function() {
console.log(i);
}
}(i))
}
}
或者
window.onload=function(){
var btns = document.getElementsByTagName('button');
for(var i = 0, len = btns.length; i < len; i++) {
function test(index){
btns[index].onclick = function() {
console.log(index);
}
}
test(i)
}
}
這樣一來每次循環(huán)的變量i
值都被封閉起來肺然,這樣在事件函數(shù)執(zhí)行時(shí)蔫缸,會(huì)查找定義時(shí)的作用域鏈
,這個(gè)作用域鏈
里的變量i
值是在每次循環(huán)中都被保留在對應(yīng)的變量對象
中际起,因此點(diǎn)擊不同的button按鈕
會(huì)輸出不同的變量i
值
如圖
閉包的缺陷
如果不是某些特定業(yè)務(wù)需求下, 盡量避免使用閉包
拾碌,因?yàn)?code>閉包在處理速度和內(nèi)存消耗
方面對腳本性能具有負(fù)面影響, 其會(huì)根據(jù)閉包
數(shù)量的多少而在內(nèi)存中創(chuàng)建更多的變量對象
, 最終可能會(huì)導(dǎo)致內(nèi)存溢出
等情況!
當(dāng)然通常最簡單的解決辦法就是: 解除對引用變量函數(shù)
的使用
引用變量函數(shù) = null;
我們可以將引用變量
的值將其設(shè)置為null
即可,js垃圾回收
將會(huì)將其清除, 釋放內(nèi)存資源!
總結(jié)閉包
1、當(dāng)內(nèi)部函數(shù)
在定義它的作用域
的外部被引用(使用)
時(shí),就創(chuàng)建了該內(nèi)部函數(shù)的閉包
,如果內(nèi)部函數(shù)
引用了位于父級函數(shù)的變量
或者其他數(shù)據(jù)時(shí),當(dāng)父級函數(shù)
調(diào)用完畢后,這些變量數(shù)據(jù)
在內(nèi)存不會(huì)被GC(Garbage collection)
釋放,因?yàn)?code>閉包它們被一直引用著!否則兩者沒有交互就不會(huì)長久存在于內(nèi)存中,所以在Chrome
中的debug
找不到閉包
2街望、通過調(diào)用閉包的內(nèi)部函數(shù)獲取到閉包的成員變量:
在閉包中返回該函數(shù)校翔,在外部接收該函數(shù)并執(zhí)行就能獲取閉包的成員變量。
原因是因?yàn)?code>詞法作用域灾前,也就是函數(shù)的作用域是其聲明的作用域而不是執(zhí)行調(diào)用時(shí)的作用域
防症。
如果我的博客對你有幫助、如果你喜歡我的博客內(nèi)容哎甲,請 “點(diǎn)贊” “評論” “收藏”
一鍵三連哦蔫敲!
如果以上內(nèi)容有任何錯(cuò)誤或者不準(zhǔn)確的地方,歡迎在下面 ?? 留個(gè)言指出炭玫、或者你有更好的想法奈嘿,歡迎一起交流學(xué)習(xí)