前言
??? 大家好,我是南木元元,熱衷分享有趣實(shí)用的文章,希望大家多多支持,一起進(jìn)步!
????個(gè)人主頁(yè):南木元元
你是否學(xué)習(xí)了很久JavaScript但還沒有搞懂閉包呢?今天就來(lái)聊一下被很多人譽(yù)為JavaScript中最難理解的概念之一的閉包。
目錄
閉包的概念
閉包產(chǎn)生的原因
作用域&作用域鏈
閉包的本質(zhì)
閉包的表現(xiàn)形式
閉包的用途
封裝私有變量
做緩存
閉包的缺點(diǎn)
結(jié)語(yǔ)
閉包的概念
- 紅寶書(P309)上對(duì)于閉包的定義
閉包指的是那些引用了另一個(gè)函數(shù)作用域中變量的函數(shù),通常是在嵌套函數(shù)中實(shí)現(xiàn)的。
- MDN對(duì)閉包的定義
閉包是指那些能夠訪問自由變量的函數(shù)。其中自由變量是指在函數(shù)中使用的,但既不是函數(shù)參數(shù)也不是函數(shù)的局部變量的變量。
總結(jié)一下就是,閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中變量的函數(shù),創(chuàng)建閉包的最常見的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù),創(chuàng)建的函數(shù)可以訪問到當(dāng)前函數(shù)的局部變量。
下面就是一個(gè)閉包的例子。
// 外部函數(shù)
function outerFunction() {
let outerVariable = 'outer';
// 內(nèi)部函數(shù)
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const innerFunc = outerFunction();
innerFunc(); // outer
在上面的代碼示例中,函數(shù)outerFunction內(nèi)部有一個(gè)innerFunction函數(shù),innerFunction函數(shù)可以訪問到outerFunction函數(shù)中的變量,此時(shí)函數(shù)innerFunction就是一個(gè)閉包。
閉包產(chǎn)生的原因
作用域&作用域鏈
首先需要知道作用域和作用域鏈的概念。
作用域就是變量與函數(shù)的可訪問范圍
在js中,有三種作用域:
- 全局作用域:變量在整個(gè)全局中都能被訪問到
- 函數(shù)作用域:變量只能在當(dāng)前函數(shù)內(nèi)被訪問到
- 塊級(jí)作用域:變量通過ES6中的let和const來(lái)聲明,只能在?對(duì)花括號(hào){ }包裹的塊中訪問
作用域鏈:從當(dāng)前作用域開始一層層往上找某個(gè)變量,如果找到全局作用域還沒找到,就放棄尋找,這種層級(jí)關(guān)系就是作用域鏈。
- 靜態(tài)作用域
js 采用的是靜態(tài)作用域(詞法作用域),即函數(shù)的作用域在函數(shù)定義時(shí)就確定了。
var num = 10;
function f1(){
console.log(num)
}
function f2(){
var num = 20;
f1()
}
f2();//10
以上代碼的執(zhí)行結(jié)果為10,這段代碼經(jīng)歷了這樣的執(zhí)行過程:
- f2函數(shù)調(diào)用,f1函數(shù)調(diào)用
- 在f1函數(shù)作用域內(nèi)查找是否有局部變量num
- 發(fā)現(xiàn)沒找到,于是根據(jù)書寫位置,向上一層作用域(全局作用域)查找,輸出10
靜態(tài)作用域也稱為詞法作用域,即在詞法分析時(shí)生成的作用域,詞法分析階段,也可以理解為代碼書寫階段,當(dāng)你把函數(shù)書寫到某個(gè)位置,不用執(zhí)行,它的作用域就已經(jīng)確定了。與之相對(duì)的是動(dòng)態(tài)作用域,函數(shù)的作?域在函數(shù)調(diào)?時(shí)才確定,如果采用動(dòng)態(tài)作用域,那么上述結(jié)果為20(如果想深入了解,可以去看這篇文章)。
在了解了js的作用域和作用域鏈后,讓我們來(lái)看看下面這段代碼:
var num = 10;
function fn() {
var num = 20;
function fun() {
console.log(num);//20
}
return fun;
}
var x = fn();
x();
上述例子中有三個(gè)作用域:全局作用域、fn的函數(shù)作用域、fun的函數(shù)作用域,它們的關(guān)系如下:
作用域鏈關(guān)系如下:
在這段代碼中,fn的作用域指向有全局作用域和它本身,而fun的作用域指向全局作用域、fn和它本身。而作用域是從最底層向上找,當(dāng)我們?cè)噲D在fun這個(gè)函數(shù)里訪問變量num的時(shí)候,此時(shí)函數(shù)作用域內(nèi)沒有num變量,當(dāng)前作用域找不到,我們需要去上層作用域(fn函數(shù)作用域)找,在這里我們找到了num為20,輸出即可(如果找到全局作用域還沒有的話就會(huì)報(bào)錯(cuò))。
閉包的本質(zhì)
問大家一個(gè)問題:那是不是只有像上述例子一樣返回函數(shù)才算是產(chǎn)生了閉包呢?
其實(shí),閉包產(chǎn)生的本質(zhì)就是:當(dāng)前環(huán)境中存在指向父級(jí)作用域的引用。因此我們還可以這么做:
var fun;
function fn() {
var num = 2;
fun = function() {
console.log(num); //2
}
}
fn();
fun();
讓fn執(zhí)行,給fun賦值后,等于說現(xiàn)在fun擁有了全局、fn和fun本身這幾個(gè)作用域的訪問權(quán)限,還是自底向上查找,最近是在fn中找到了num,因此輸出2。
在這里是外面的變量fun存在著父級(jí)作用域的引用,因此產(chǎn)生了閉包,形式變了,本質(zhì)沒有改變。
閉包的表現(xiàn)形式
明白了本質(zhì)后,那我們思考下,實(shí)際場(chǎng)景中,閉包是如何體現(xiàn)的呢?
- 返回一個(gè)函數(shù)(上面已經(jīng)舉例)
- 作為函數(shù)參數(shù)傳遞
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 這就是閉包
fn();
}
// 輸出2,而不是1
foo();
- 定時(shí)器、事件監(jiān)聽或者任何異步中,只要使用了回調(diào)函數(shù),實(shí)際上就是在使用閉包。
比如以下的閉包保存的僅僅是window和當(dāng)前作用域。
// 定時(shí)器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件監(jiān)聽
$('#btn').click(function(){
console.log('222');
})
- IIFE(立即執(zhí)行函數(shù)表達(dá)式)創(chuàng)建閉包,保存了全局作用域window和當(dāng)前的函數(shù)作用域,因此可以使用全局的變量。
var a = 2;
(function IIFE(){
// 輸出2
console.log(a);
})();
現(xiàn)在,你是否會(huì)感嘆一句:好家伙,原來(lái)我用了閉包這么多年!
閉包的用途
閉包有兩個(gè)常用的用途:
- 封裝私有變量
- 做緩存
封裝私有變量
閉包可以使我們?cè)诤瘮?shù)外部能夠訪問到函數(shù)內(nèi)部的變量。通過使用閉包,可以通過在外部調(diào)用閉包函數(shù),從而在外部訪問到函數(shù)內(nèi)部的變量,可以使用這種方法來(lái)創(chuàng)建私有變量,以防止其被外部訪問和修改。
在下面這個(gè)例子中,調(diào)用函數(shù),輸出的結(jié)果都是1,但是我們的代碼效果是想讓count
每次加一。
function add() {
let count = 0;
count++;
console.log(count);
}
add() //輸出1
add() //輸出1
add() //輸出1
一種顯而易見的方法是將count
提到函數(shù)體外,作為全局變量。這么做當(dāng)然是可以解決問題,但是在實(shí)際開發(fā)中,一個(gè)項(xiàng)目由多人共同開發(fā),你不清楚別人定義的變量名稱是什么,很容易沖突,有什么其他的辦法可以解決這個(gè)問題呢?
function add(){
let count = 0
function a(){
count++
console.log(count);
}
return a
}
var res = add()
res() //1
res() //2
res() //3
答案是用閉包。在上面的代碼示例中,add
函數(shù)返回了一個(gè)閉包a,其中包含了count
變量。由于count
只在add
函數(shù)內(nèi)部定義,因此外部無(wú)法直接訪問它。但是,由于a
函數(shù)引用了count
變量,因此count
變量的值可以在閉包內(nèi)部被修改和訪問。這種方式可以用于封裝一些私有的數(shù)據(jù)和邏輯。
做緩存
函數(shù)一旦被執(zhí)行完畢,其內(nèi)存就會(huì)被銷毀。而閉包可以使已經(jīng)運(yùn)行結(jié)束的函數(shù)上下文中的變量對(duì)象繼續(xù)留在內(nèi)存中,因?yàn)殚]包函數(shù)保留了這個(gè)變量對(duì)象的引用,所以這個(gè)變量對(duì)象不會(huì)被回收。
function foo(){
var myName ='張三';
let test = 1;
var innerBar={
getName: function(){
console.log(test);
return myName;
},
setName:function(newName){
myName = newName;
}
}
return innerBar;
}
var bar = foo();
console.log(bar.getName()); //1 張三
bar.setName('李四');
console.log(bar.getName()); //1 李四
這里var bar = foo() 執(zhí)行完后本來(lái)應(yīng)該被銷毀,但是因?yàn)樾纬闪碎]包,所以導(dǎo)致foo執(zhí)行上下文沒有被銷毀干凈,被引用了的變量myName、test沒被銷毀,閉包里存放的就是變量myName、test,這個(gè)閉包就像是setName、getName的專屬背包,setName、getName依然可以使用foo執(zhí)行上下文中的test和myName。
閉包的應(yīng)用是非常廣泛的,比如常見的防抖和節(jié)流等其實(shí)也都是閉包的應(yīng)用。
閉包的缺點(diǎn)
閉包也存在著一個(gè)潛在的問題,由于閉包會(huì)引用外部函數(shù)的變量,但是這些變量在外部函數(shù)執(zhí)行完畢后沒有被釋放,那么這些變量會(huì)一直存在于內(nèi)存中,這可能會(huì)帶來(lái)內(nèi)存泄漏問題,因此,需要及時(shí)釋放閉包,即手動(dòng)調(diào)用閉包函數(shù),并將其返回值賦值為null,這樣可以讓閉包中的變量及時(shí)被垃圾回收器回收。
結(jié)語(yǔ)
本文主要介紹了被譽(yù)為JavaScript中最難理解的概念之一的閉包,閉包的表現(xiàn)形式多樣、應(yīng)用廣泛,日常開發(fā)中其實(shí)都有閉包的身影,在實(shí)際的開發(fā)過程中,合理地使用閉包可以幫助我們更加高效地編寫代碼,提高程序的性能和可維護(hù)性。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-751652.html
??如果此文對(duì)你有幫助的話,歡迎??關(guān)注、??點(diǎn)贊、?收藏、??評(píng)論,支持一下博主~?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-751652.html
到了這里,關(guān)于我從來(lái)不理解JavaScript閉包,但我用了它好多年的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!