還記得開始學(xué)習(xí)前端吕朵,閉包作用域神馬的都非常難以理解绰垂,遇到面試只能提前去背嗅榕,其實(shí)被一仔細(xì)問都很難說出所以然顺饮,相信很多新手都有和我曾經(jīng)一樣的問題,所以我們今天來學(xué)習(xí)一下閉包和函數(shù)誊册,一起來了解這一部分吧~
????我們其實(shí)每天都在寫閉包领突,每天編碼都無時(shí)無刻的享受著閉包帶來的便利,我們的動(dòng)畫處理案怯,事件回調(diào)君旦,包括在一些框架中閉包一直都存在,閉包和作用域規(guī)則息息相關(guān)嘲碱,所以我們會(huì)連帶了解一下作用域相關(guān)知識(shí)金砍。
因卓誒;此文已同步到因卓誒blog?點(diǎn)擊訪問原文麦锯,獲得更好體驗(yàn)
?理解閉包
??????閉包允許函數(shù)訪問函數(shù)外部的變量恕稠,只要變量存在于聲明函數(shù)的作用域中即可訪問。所聲明的函數(shù)不會(huì)因?yàn)樽饔糜虻南ФХ鲂溃侨魏螘r(shí)間都可以調(diào)用鹅巍。
var?value?=?"我是外部變量";
function?testFun(){
console.log(value);
}
testFun(); // 執(zhí)行函數(shù)
這樣子的代碼我們每天都在寫,其實(shí)它就是一個(gè)閉包料祠,我們可以把代碼套入到剛剛的概念中; 首先函數(shù)聲明和變量都在全局作用域中(符合在同樣的作用域概念)也滿足任何時(shí)間去調(diào)用testFun函數(shù)骆捧,但是我們拿這種簡單的例子體現(xiàn)不出閉包的作用。
// 一個(gè)更為直觀的閉包例子
var?value?=?"shenhao";
var?later = null;
function?outsideFun(){
??var?innerValue?=?"函數(shù)作用域聲明的變量";
??function?insideFun(){
????//?由于作用域鏈
????console.log(value); // shenhao
????console.log(innervalue); // 函數(shù)作用域聲明的變量
??}
??//?把內(nèi)部函數(shù)傳遞給later
??later = insideFun;
}
outsideFun();
later();
按照道理來說髓绽,執(zhí)行l(wèi)ater時(shí)候訪問變量敛苇,value肯定是有值的,因?yàn)樗嬖谟谌汁h(huán)境中顺呕,那么innerValue真的因?yàn)橥獠孔饔糜虻南Ф莕ull么枫攀,答案是不可能的,由于閉包原因株茶,我們還能訪問innerValue的值来涨。所以我們能得出一個(gè)結(jié)論:?當(dāng)在外部函數(shù)內(nèi)部創(chuàng)建一個(gè)函數(shù)的時(shí)候,還額外的創(chuàng)建了一個(gè)閉包忌卤,這個(gè)閉包包含了創(chuàng)建它時(shí)此作用域全部的變量扫夜,當(dāng)這個(gè)作用域消失的時(shí)候,那么閉包中還在驰徊,因此我們就可以在作用域消失之后還能訪問變量啦笤闯。
??那我們可以用一個(gè)圖來描述一下這個(gè)關(guān)系(此圖借鑒了js忍者秘籍第5章閉包相關(guān)內(nèi)容的氣泡圖)
閉包就可以使用這個(gè)圖來解釋,那么我們必須要記住的一點(diǎn)就是棍厂,通過閉包來訪問變量的函數(shù)都有一個(gè)作用域鏈颗味,作用域鏈保存著閉包的信息。
?????使用閉包的時(shí)候牺弹,所有的信息都會(huì)存儲(chǔ)在內(nèi)存中浦马,那么這些信息什么時(shí)候被銷毀,取決于JS引擎認(rèn)為這些信息不再使用才會(huì)被回收或者當(dāng)頁面卸載時(shí)张漂。
用閉包實(shí)現(xiàn)私有變量
開發(fā)者如果不止會(huì)一種編程語言晶默,那么肯定就知道私有變量這個(gè)概念,但是在JS中是不存在的航攒,但是我們可以使用閉包來構(gòu)建一個(gè)“類私有變量”磺陡,但是并不是完美真正的私有變量,我們看一下demo
function?Test(){
? var?count?=?0;
??this.addCount?=?function(){
????count?++;
??}
??this.getCount?=?function(){
????return?count;
??}
}
const?private?=?new?Test();
private.addCount();
private.getCount(); // 1
??? Test方法中的count就是我們所說的私有屬性漠畜,只能通過內(nèi)置的add和get方法才能對(duì)其操作币他,那么我們也可以構(gòu)造多個(gè)函數(shù),那么當(dāng)然不同函數(shù)中的count都是具有獨(dú)立性的憔狞。我們通過上述代碼簡單實(shí)現(xiàn)了一個(gè)私有屬性的例子蝴悉,就是通過閉包,外部通過內(nèi)部的函數(shù)去獲取/修改變量而外部不能直接去更改變量瘾敢。
回調(diào)函數(shù)callBack
當(dāng)我們做WEB應(yīng)用時(shí)候拍冠,做一些動(dòng)畫的需求,常見地要求我們同時(shí)操作好幾個(gè)DOM簇抵,操作好幾個(gè)動(dòng)畫狀態(tài)庆杜,那我們在學(xué)習(xí)閉包之前,都是在全局作用域去定義變量正压,然后去調(diào)用多個(gè)函數(shù)欣福,對(duì)應(yīng)的函數(shù)再去改變對(duì)應(yīng)的變量,這樣非常不好焦履,會(huì)造成代碼過于繁重且控制多個(gè)變量會(huì)非常麻煩拓劝。那么我們可以用閉包解決這個(gè)問題,假設(shè)我們需要改變2個(gè)動(dòng)畫區(qū)域嘉裤,我們可以這么寫郑临。
function?animated(elementID){
??var?elem?=?document.getElementById(elementID);
??var tick = 0;
??var?timer?=?setInterval(() => {
?? ...
??})
}
如果我們把tick和timer等變量挪到全局作用域中,那么我們控制多個(gè)elment的動(dòng)畫屑宠,將會(huì)產(chǎn)生大量的變量厢洞,如果我們通過閉包,計(jì)時(shí)器中的回調(diào)都可以操作對(duì)應(yīng)自己的私有變量,那么我們就可以這樣得出結(jié)論:
閉包不是一個(gè)創(chuàng)建函數(shù)時(shí)候的快照躺翻,而是一個(gè)可以在函數(shù)執(zhí)行的時(shí)候能夠修改變量的狀態(tài)封裝丧叽,只要閉包存在,我們就可以修改變量的值
執(zhí)行上下文跟蹤代碼
我們都知道JS是單線程的執(zhí)行模型公你,我們都知道JS有2種代碼類型踊淳,一種是全局代碼,一種是函數(shù)代碼陕靠,那么執(zhí)行上下文也分為全局執(zhí)行上下文和函數(shù)執(zhí)行上下文迂尝,全局執(zhí)行上下文則js程序開始時(shí)就創(chuàng)建了,函數(shù)執(zhí)行上下文那么就是函數(shù)執(zhí)行開始創(chuàng)建剪芥,那么我們可以看一個(gè)例子來了解一下:
<script>
// 全局執(zhí)行上下文開始
var start = "開始";
function?method(){
??//?函數(shù)執(zhí)行文開始
}
method();
</script>
當(dāng)一個(gè)函數(shù)開始執(zhí)行垄开,則創(chuàng)建一個(gè)新的執(zhí)行上下文,執(zhí)行完畢則會(huì)關(guān)閉上下文回到創(chuàng)建時(shí)的執(zhí)行上下文接著執(zhí)行代碼税肪,所以JS為了跟蹤這種調(diào)用順序就要使用執(zhí)行上下文棧溉躲,也是大家熟悉的 “調(diào)用棧”寸认;
(PS:有那味了)
就像一個(gè)托盤一樣签财,盤子越壘越高,每一個(gè)盤子都是函數(shù)執(zhí)行棧偏塞,執(zhí)行完以后最終都會(huì)回歸到全局執(zhí)行棧中
執(zhí)行上下文除了可以定位執(zhí)行的函數(shù)之外唱蒸,在靜態(tài)環(huán)境下也可以準(zhǔn)確地定位到具體的標(biāo)識(shí)符噢。
作用域(詞法環(huán)境)
詞法環(huán)境又稱之為作用域灸叼,是js引擎內(nèi)部實(shí)現(xiàn)的一套跟蹤標(biāo)識(shí)符和變量映射關(guān)系的機(jī)制神汹,很多文章都沒有把詞法環(huán)境的概念講清楚,那么表達(dá)“映射關(guān)系”是最貼切的古今。
作用域是和js代碼息息相關(guān)的屁魏,那么在ES6初版的時(shí)候沒有塊級(jí)作用域的解決方案,所以從C捉腥,Java開發(fā)轉(zhuǎn)過來的開發(fā)者們就會(huì)覺得JS應(yīng)該也有這樣的概念氓拼,但是其實(shí)從ES6的let const之后才解決了這個(gè)問題。我們都知道這種作用域場景最直觀的表達(dá)就是代碼嵌套抵碟,那么我們寫一個(gè)例子:
var?all?=?"全局的變量"
function a(){
?var aValue = "a函數(shù)里面的變量"
?function b(){
???var?bValue = "b函數(shù)里面的變量"
???for(var?c?=?0;?c<=10;?c++){
?????console.log(all?+?aValue?+?bvalue + c);
???}
?}
}
JS引擎是如何跟蹤這些代碼桃漾,分析它們的結(jié)構(gòu),這就是詞法環(huán)境的作用拟逮。
執(zhí)行上下文對(duì)應(yīng)的詞法環(huán)境撬统,詞法環(huán)境中包含了上下文中定義標(biāo)識(shí)符映射表,所以for循環(huán)具有a和b的標(biāo)識(shí)符映射表敦迄,無論何時(shí)創(chuàng)建了函數(shù)恋追,那么會(huì)有一個(gè)與之對(duì)應(yīng)的詞法環(huán)境凭迹,那么這個(gè)詞法環(huán)境會(huì)存儲(chǔ)在[[Enviroment]]這個(gè)內(nèi)部屬性上,所以這就是JS引擎分析代碼之間關(guān)聯(lián)的重要原因苦囱。
那么現(xiàn)在我們已經(jīng)了解了基本的標(biāo)識(shí)符查找規(guī)則嗅绸,那么變量類型也是我們需要簡單了解的,盡管這比較簡單沿彭。
理解JS的變量類型
像我們現(xiàn)在討論的這個(gè)話題朽砰,基本是中小公司面試必問的尖滚,爛大街的let
?conat var 有什么區(qū)別喉刘?這個(gè)問題出鏡率實(shí)在太多了,網(wǎng)上給的答案也太多了漆弄,我們就根據(jù)剛剛所提到的知識(shí)點(diǎn)來總結(jié)一下:
可變性
詞法環(huán)境
const是無法改變的
let,const 比較var的區(qū)別則就是詞法環(huán)境不一樣睦裳,let和const解決了JS塊級(jí)作用域的缺陷。
社區(qū)中很多大佬或者某些公司的代碼規(guī)范中撼唾,出現(xiàn)了強(qiáng)制要求使用const替換var和let的事情廉邑,因?yàn)榉浅6啻a是因?yàn)樽兞康目勺冃栽斐傻腂UG,那么我們在日常開發(fā)中倒谷,其實(shí)是很少遇到只能通過變量的可變來做的需求蛛蒙,所以我認(rèn)為這種觀念還是蠻正確的,值得提倡
那么我們來深入const的工作原理渤愁,深層次的了解一下:
我們一般使用const是這樣的牵祟,第一是不會(huì)再次賦值的特殊變量或者是一個(gè)固定的值,比如我們在運(yùn)算的時(shí)候抖格,需要定義人數(shù)诺苹,數(shù)量諸如此類,也是為了JS代碼的運(yùn)行穩(wěn)定為程序優(yōu)化提供便利雹拄。
const a = "一次賦值";
a = 1; // 重新賦一個(gè)新值 報(bào)錯(cuò)
const?b = {};
b.age = 20; // 不會(huì)報(bào)錯(cuò)
const c = [];
c.pus("1");?//?不會(huì)報(bào)錯(cuò)
const只允許初始化賦一次值收奔,以后如何有“新值”賦給const變量,則會(huì)報(bào)錯(cuò)滓玖,但是在const變量上對(duì)對(duì)象坪哄,數(shù)組的修改是允許的。
變量的可變性已經(jīng)描述完了势篡,現(xiàn)在我們可以看看變量的詞法環(huán)境的區(qū)別翩肌。
當(dāng)我們使用關(guān)鍵字var的時(shí)候,該變量的詞法環(huán)境是最近的函數(shù)內(nèi)部或者全局定義的殊霞,那么我們就可以用一個(gè)demo表示:
function?fun(){
??for(var?i?=?0;i<10;i++){
????var?thisMessage?=?"和變量i一個(gè)作用域"
??}
}
1.?從這個(gè)例子就可以看出摧阅,變量i是var定義的,所以它屬于最近的函數(shù)詞法環(huán)境內(nèi)绷蹲,所以變量i和thisMessage是同一個(gè)作用域棒卷,for循環(huán)中的變量i也因此忽略塊級(jí)作用域
所以很多文章教程中顾孽,包括我們?nèi)粘i_發(fā)的時(shí)候,在寫for循環(huán)中類似的臨時(shí)變量的時(shí)候比规,需要使用let定義變量若厚,避免在多個(gè)循環(huán)中(都在一個(gè)函數(shù)詞法環(huán)境中)會(huì)造成沖突。
那么我們使用let改寫此demo之后:
for(let?i?=?0;?i<10;i++)
那么我們可以在一個(gè)詞法環(huán)境中寫多個(gè)類似的代碼蜒什,從而不造成沖突测秸,外部詞法環(huán)境也無法訪問到i變量。
我們已經(jīng)了解了標(biāo)識(shí)符的定義和詞法環(huán)境相關(guān)知識(shí)灾常,那么我們下來應(yīng)該進(jìn)行深入探索在詞法環(huán)境中如何注冊標(biāo)識(shí)符霎冯。
注冊標(biāo)識(shí)符
我們?nèi)绻菑腏ava或者C轉(zhuǎn)JS的童鞋們,當(dāng)我們第一次接觸到下面的代碼钞瀑,你或許感覺到懵逼沈撞。
var?test?=?"我是一個(gè)測試變量";
testFun(test);
function?testFun(str){
??return "Hello" + str;
}
為什么我的函數(shù)還沒定義,就可以執(zhí)行雕什?我們都知道JS是逐行執(zhí)行代碼的缠俺,按道理說函數(shù)還沒執(zhí)行到聲明這一塊,是不可以調(diào)用testFun贷岸。那么JS是如何注冊函數(shù)的壹士,如何知道testFun的存在的呢?
JS在創(chuàng)建新的詞法環(huán)境的時(shí)候偿警,會(huì)執(zhí)行2步躏救。
?js會(huì)訪問并注冊在當(dāng)前詞法環(huán)境注冊的變量和函數(shù)
執(zhí)行代碼:取決于變量類型和詞法環(huán)境類型
那我們要細(xì)細(xì)分析一下這個(gè)注冊過程。
? ? 1. 首先如果是創(chuàng)建了函數(shù)詞法環(huán)境户敬,那么JS先會(huì)定義行參和參數(shù)默認(rèn)值
????2. 如果是全局詞法環(huán)境落剪,那么將會(huì)把函數(shù)聲明的代碼查詢到(函數(shù)表達(dá)式或者是箭頭函數(shù)都不會(huì)被查到)查詢到之后將會(huì)把值賦給函數(shù)同名的標(biāo)識(shí)符中,如果標(biāo)識(shí)符已存在將會(huì)被改寫尿庐,如果是塊級(jí)作用域則跳過此步驟忠怖。
? ? 3. 變量聲明會(huì)在函數(shù)和全局作用域中,找到當(dāng)前函數(shù)和其他函數(shù)中使用var聲明的變量以及在其他函數(shù)或者代碼塊之外的let和const變量抄瑟,在塊級(jí)作用域中查找let和const變量凡泣,如果不存在將賦值為undefined,存在即保留皮假。
總結(jié):JS注冊標(biāo)識(shí)符將依據(jù)當(dāng)前詞法環(huán)境進(jìn)行注冊鞋拟。
所以我們就可以解釋我們剛剛寫的demo出現(xiàn)的疑惑了,是因?yàn)镴S在第一階段中加載了全局詞法環(huán)境惹资,將會(huì)把函數(shù)聲明語句賦給一個(gè)新的標(biāo)識(shí)符贺纲,因此我們在第二個(gè)階段代碼執(zhí)行時(shí)能順利地取到函數(shù)。
那么我們在開發(fā)中不僅僅會(huì)遇到上面這種情況褪测,我們可能還會(huì)遇到函數(shù)標(biāo)識(shí)符重載的問題:
console.log(typeof fun); // function
var fun = 3;
function?fun(){}
console.log(typeof fun); // number
這就涉及到一個(gè)概念叫做變量提升猴誊,例如函數(shù)聲明將提升到全局作用域頂部潦刃,變量聲明將提升到函數(shù)作用域頂部
那么我們來分析一下上述的代碼:
首先在第一個(gè)階段,函數(shù)聲明語句將把函數(shù)賦給同名標(biāo)識(shí)符懈叹,所以在代碼執(zhí)行的時(shí)候fun標(biāo)識(shí)符是有值的而且類型就是函數(shù)
然后當(dāng)進(jìn)行變量處理的時(shí)候乖杠,發(fā)現(xiàn)fun標(biāo)識(shí)符已被注冊所以不會(huì)被賦值undefind,當(dāng)代碼執(zhí)行時(shí)fun已被再次賦值為3澄成,所以第二個(gè)console打印出的類型是number
閉包工作原理
我們之前了解過閉包可以讓我們訪問函數(shù)創(chuàng)建的作用域的全部變量胧洒,也舉了幾個(gè)例子,比如動(dòng)畫處理和回調(diào)墨状。
我們將利用詞法環(huán)境和執(zhí)行上下文重新分析我們之前的demo卫漫,這將會(huì)對(duì)我們理解閉包會(huì)更有幫助。
1. 計(jì)數(shù)器私有變量的例子
function Test(){
var count = 0;
this.addCount = function(){
count ++;
}
this.getCount = function(){
return count;
}
}
分析:我們通過new關(guān)鍵字調(diào)用Test這個(gè)構(gòu)造函數(shù)歉胶,我們在外部訪問addCount函數(shù)實(shí)際上就是創(chuàng)建了這個(gè)閉包汛兜,在我們執(zhí)行Test.addCount這樣的函數(shù)之前我們的執(zhí)行上下文是全局執(zhí)行上下文,因?yàn)槲覀冋{(diào)用函數(shù)會(huì)創(chuàng)建新的執(zhí)行上下文那么也會(huì)創(chuàng)建新的詞法環(huán)境通今,那么addCount這個(gè)詞法環(huán)境包含著創(chuàng)建這個(gè)函數(shù)的詞法環(huán)境,當(dāng)在addCount中找不到count變量那么就會(huì)從外部環(huán)境去尋找(因?yàn)榇藭r(shí)對(duì)象是活躍的肛根,count變量就在此對(duì)象中)辫塌,就是這么簡單...
我們理解了執(zhí)行上下文和詞法環(huán)境的作用,詞法環(huán)境主要的作用就是跟蹤函數(shù)中定義的變量派哲,新的執(zhí)行環(huán)境將創(chuàng)建新的詞法環(huán)境臼氨。
請(qǐng)注意,JS沒有真正的私有變量芭届,上面的demo我們可以改變它:
var testImport = {};
var?Test?=?new?Test();
testImport.getCount?=?Test.getCount
console.log(testImport.getCount); // 可以訪問
2. 動(dòng)畫處理的例子
function animated(elementID){
var elem = document.getElementById(elementID);
var tick = 0;
var timer = setInterval(() => {
...
})
}
animated("box1");
animated("box2");
分析:我們調(diào)用了animated的2個(gè)方法也就創(chuàng)建了2個(gè)詞法環(huán)境储矩,同樣的我們也創(chuàng)建了新的執(zhí)行上下文,每個(gè)詞法環(huán)境都有對(duì)應(yīng)的elem褂乍,tick等變量持隧,我們在這個(gè)代碼中設(shè)置了setInterval這個(gè)函數(shù),只要有一個(gè)函數(shù)在訪問變量逃片,那么這個(gè)閉包就會(huì)一直存在屡拨,除非計(jì)時(shí)器被清除,隨后回調(diào)函數(shù)被執(zhí)行褥实,然后這個(gè)閉包會(huì)訪問創(chuàng)建閉包時(shí)候的變量呀狼,極大的簡化了代碼。
小結(jié)
我們徹底理解了閉包的概念损离,也通過安全氣泡的方法來加深我們對(duì)于詞法環(huán)境的理解哥艇,保存創(chuàng)建函數(shù)時(shí)的詞法環(huán)境我們是存儲(chǔ)[[Environment]]屬性中的。所以我們當(dāng)作用域消失還能訪問到創(chuàng)建時(shí)候的變量
????2. JS引擎通過執(zhí)行上下文棧(調(diào)用棧)來跟蹤代碼的執(zhí)行僻澎,每次調(diào)用函數(shù)都會(huì)有新的執(zhí)行上下文貌踏,并且像我們演示服務(wù)生托盤一樣推到最頂端
????3.?JS通過詞法環(huán)境來跟蹤標(biāo)識(shí)符(也就是作用域)
????4. 我們可以定義全局瓮增,函數(shù),局部作用域級(jí)別的變量
??? 5. 使用let const var關(guān)鍵字聲明變量時(shí)哩俭,我們理解了它們的區(qū)別:可再次賦值和是否可以局部作用域绷跑。