引子
我們先來看一個例子:
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
上面代碼中辉阶,函數(shù)f1
可以讀取全局變量n
练链。但是翔脱,函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量。
function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined
上面代碼中兑宇,函數(shù)f1
內(nèi)部聲明的變量n
碍侦,函數(shù)外是無法讀取的。
如果有時需要得到函數(shù)內(nèi)的局部變量隶糕。正常情況下瓷产,這是辦不到的,只有通過變通方法才能實現(xiàn)枚驻。那就是在函數(shù)的內(nèi)部濒旦,再定義一個函數(shù)。
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
上面代碼中再登,函數(shù)f2
就在函數(shù)f1
內(nèi)部尔邓,這時f1
內(nèi)部的所有局部變量,對f2都是可見的锉矢。既然f2
可以讀取f1
的局部變量梯嗽,那么只要把f2
作為返回值,我們就可以在f1
外部讀取它的內(nèi)部變量了沽损。
閉包是什么
我們可以對上面代碼進(jìn)行如下修改:
function f1() {
var a = 999;
function f2() {
console.log(a);
}
return f2; // f1返回了f2的引用
}
var result = f1(); // result就是f2函數(shù)了
result(); // 執(zhí)行result灯节,全局作用域下沒有a的定義,
//但是函數(shù)閉包,能夠把定義函數(shù)的時候的作用域一起記住炎疆,輸出999
上面代碼中卡骂,函數(shù)f1
的返回值就是函數(shù)f2
,由于f2
可以讀取f1
的內(nèi)部變量形入,所以就可以在外部獲得f1
的內(nèi)部變量了全跨。
閉包就是函數(shù)f2
,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)亿遂。由于在JavaScript語言中浓若,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量,因此可以把閉包簡單理解成“定義在一個函數(shù)內(nèi)部的函數(shù)”崩掘。閉包最大的特點七嫌,就是它可以“記住”誕生的環(huán)境,比如f2
記住了它誕生的環(huán)境f1
苞慢,所以從f2
可以得到f1
的內(nèi)部變量诵原。在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來的一座橋梁挽放。
那到底什么是閉包呢绍赛?
當(dāng)一個函數(shù)能夠記住并訪問到其所在的詞法作用域及作用域鏈,特別強調(diào)是在其定義的作用域外進(jìn)行的訪問辑畦,此時該函數(shù)和其上層執(zhí)行上下文共同構(gòu)成閉包吗蚌。
閉包就是函數(shù)中的函數(shù),里面的函數(shù)可以訪問外面函數(shù)的變量纯出,外面的變量的是這個內(nèi)部函數(shù)的一部分蚯妇。
閉包形成的條件
- 函數(shù)嵌套
- 內(nèi)部函數(shù)引用外部函數(shù)的局部變量
閉包的特性
每個函數(shù)都是閉包,每個函數(shù)天生都能夠記憶自己定義時所處的作用域環(huán)境暂筝。把一個函數(shù)從它定義的那個作用域箩言,挪走,運行焕襟。這個函數(shù)能夠記憶住定義時的那個作用域陨收。不管函數(shù)走到哪里,定義時的作用域就帶到了哪里鸵赖。
//例題1
var inner;
function outer() {
var a=250;
inner=function() {
alert(a);//這個函數(shù)雖然在外面執(zhí)行务漩,但能夠記憶住定義時的那個作用域,a是250
}
}
outer();
var a=300;
inner();//一個函數(shù)在執(zhí)行的時候它褪,找閉包里面的變量饵骨,不會理會當(dāng)前作用域。
//例題2
function outer(x) {
function inner(y) {
console.log(x+y);
}
return inner;
}
var inn = outer(3); // 數(shù)字3傳入outer函數(shù)后茫打,inner函數(shù)中x便會記住這個值
inn(5); // 當(dāng)inner函數(shù)再傳入5的時候宏悦,只會對y賦值镐确,所以最后彈出8
閉包的內(nèi)存泄漏
棧內(nèi)存提供一個執(zhí)行環(huán)境包吝,即作用域饼煞,包括全局作用域和私有作用域,那他們什么時候釋放內(nèi)存的诗越?
- 全局作用域----只有當(dāng)頁面關(guān)閉的時候全局作用域才會銷毀
- 私有的作用域----只有函數(shù)執(zhí)行才會產(chǎn)生
一般情況下砖瞧,函數(shù)執(zhí)行會形成一個新的私有的作用域,當(dāng)私有作用域中的代碼執(zhí)行完成后嚷狞,我們當(dāng)前作用域都會主動的進(jìn)行釋放和銷毀块促。但當(dāng)遇到函數(shù)執(zhí)行返回了一個引用數(shù)據(jù)類型的值,并且在函數(shù)的外面被一個其他的東西給接收了床未,這種情況下一般形成的私有作用域都不會銷毀竭翠。
如下面這種情況:
function fn() {
var num=100;
return function() {}
}
var f = fn(); // fn執(zhí)行形成的這個私有的作用域就不能再銷毀了
也就是像上面這段代碼,fn
函數(shù)內(nèi)部的私有作用域會被一直占用的薇搁,發(fā)生了內(nèi)存泄漏斋扰。所謂內(nèi)存泄漏指任何對象在您不再擁有或需要它之后仍然存在。閉包不能濫用啃洋,否則會導(dǎo)致內(nèi)存泄露传货,影響網(wǎng)頁的性能。閉包使用完后宏娄,要立即釋放資源问裕,將引用變量指向null
。
接下來我們看下有關(guān)于內(nèi)存泄漏的一道經(jīng)典面試題:
function outer() {
var num=0; // 內(nèi)部變量
return function add() { // 通過return返回add函數(shù)孵坚,就可以在outer函數(shù)外訪問了
num++;//內(nèi)部函數(shù)有引用粮宛,作為add函數(shù)的一部分了
console.log(num);
};
}
var func1 = outer();
func1(); // 實際上是調(diào)用add函數(shù), 輸出1
func1(); // 輸出2 因為outer函數(shù)內(nèi)部的私有作用域會一直被占用
var func2 = outer();
func2(); // 輸出1 每次重新引用函數(shù)的時候卖宠,閉包是全新的巍杈。
func2(); // 輸出2
閉包的作用
- 可以讀取函數(shù)內(nèi)部的變量
- 可以使變量的值長期保存在內(nèi)存中,生命周期比較長逗堵。因此不能濫用閉包秉氧,否則會造成網(wǎng)頁的性能問題
- 可以用來實現(xiàn)JS模塊
JS模塊:具有特定功能的js文件,將所有的數(shù)據(jù)和功能都封裝在一個函數(shù)內(nèi)部(私有的),只向外暴露一個包信n
個方法的對象或函數(shù)蜒秤,模塊的使用者汁咏,只需要通過模塊暴露的對象調(diào)用方法來實現(xiàn)對應(yīng)的功能。
//index.html文件
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
myModule2.doSomething()
myModule2.doOtherthing()
</script>
//myModule.js文件
(function () {
var msg = 'Beijing'//私有數(shù)據(jù)
//操作數(shù)據(jù)的函數(shù)
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}
//向外暴露對象(給外部使用的兩個方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})()
閉包的運用
我們要實現(xiàn)這樣的一個需求: 點擊某個按鈕作媚,提示"點擊的是第n
個按鈕"攘滩,此處我們先不用事件代理:
.....
<button>測試1</button>
<button>測試2</button>
<button>測試3</button>
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = function () {
console.log('第' + (i + 1) + '個')
}
}
</script>
點擊任意一個按鈕,后臺都是彈出“第四個”纸泡,這是因為i
是全局變量,執(zhí)行到點擊事件時漂问,此時i
的值為3。那該如何修改,最簡單的是用let
聲明i
蚤假。
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = function() {
console.log('第' + (i + 1) + '個')
}
}
另外我們可以通過閉包的方式來修改:
for (var i = 0; i < btns.length; i++) {
(function (j) {
btns[j].onclick = function () {
console.log('第' + (j + 1) + '個')
}
})(i)
}