閉包是什么
在 JavaScript 中园爷,閉包是一個(gè)讓人很難弄懂的概念按声。ECMAScript 中給閉包的定義是:閉包算凿,指的是詞法表示包括不被計(jì)算的變量的函數(shù),也就是說(shuō),函數(shù)可以使用函數(shù)之外定義的變量滚粟。
是不是看完這個(gè)定義感覺(jué)更加懵逼了?別急梯轻,我們來(lái)分析一下划鸽。
- 閉包是一個(gè)函數(shù)
- 閉包可以使用在它外面定義的變量
- 閉包存在定義該變量的作用域中
好像有點(diǎn)清晰了,但是使用在它外面定義的變量是什么意思弦赖,我們先來(lái)看看變量作用域项栏。
變量作用域
變量可分為全局變量和局部變量。全局變量的作用域就是全局性的蹬竖,在 js 的任何地方都可以使用全局變量沼沈。在函數(shù)中使用 var 關(guān)鍵字聲明變量流酬,這時(shí)的變量即是局部變量,它的作用域只在聲明該變量的函數(shù)內(nèi)列另,在函數(shù)外面是訪問(wèn)不到該變量的芽腾。
var func = function(){
var a = 'linxin';
console.log(a); // linxin
}
func();
console.log(a); // Uncaught ReferenceError: a is not defined
作用域相對(duì)比較簡(jiǎn)單,我們不多講页衙,來(lái)看看跟閉包關(guān)系比較大的變量生存周期摊滔。
變量生存周期
全局變量,生命周期是永久的店乐。局部變量艰躺,當(dāng)定義該變量的函數(shù)調(diào)用結(jié)束時(shí),該變量就會(huì)被垃圾回收機(jī)制回收而銷毀眨八。再次調(diào)用該函數(shù)時(shí)又會(huì)重新定義了一個(gè)新變量腺兴。
var func = function(){
var a = 'linxin';
console.log(a);
}
func();
a 為局部變量,在 func 調(diào)用完之后踪古,a 就會(huì)被銷毀了含长。
var func = function(){
var a = 'linxin';
var func1 = function(){
a += ' a';
console.log(a);
}
return func1;
}
var func2 = func();
func2(); // linxin a
func2(); // linxin a a
func2(); // linxin a a a
可以看出,在第一次調(diào)用完 func2 之后伏穆,func 中的變量 a 變成 'linxin a'拘泞,而沒(méi)有被銷毀。因?yàn)榇藭r(shí) func1 形成了一個(gè)閉包枕扫,導(dǎo)致了 a 的生命周期延續(xù)了陪腌。
這下子閉包就比較明朗了。
- 閉包是一個(gè)函數(shù)烟瞧,比如上面的 func1 函數(shù)
- 閉包使用其他函數(shù)定義的變量诗鸭,使其不被銷毀。比如上面 func1 調(diào)用了變量 a
- 閉包存在定義該變量的作用域中参滴,變量 a 存在 func 的作用域中强岸,那么 func1 也必然存在這個(gè)作用域中。
現(xiàn)在可以說(shuō)砾赔,滿足這三個(gè)條件的就是閉包了蝌箍。
下面我們通過(guò)一個(gè)簡(jiǎn)單而又經(jīng)典的例子來(lái)進(jìn)一步熟悉閉包。
for (var i = 0; i < 4; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}
我們可能會(huì)簡(jiǎn)單的以為控制臺(tái)會(huì)打印出 0 1 2 3暴心,可事實(shí)卻打印出了 4 4 4 4妓盲,這又是為什么呢?我們發(fā)現(xiàn)专普,setTimeout 函數(shù)時(shí)異步的悯衬,等到函數(shù)執(zhí)行時(shí),for循環(huán)已經(jīng)結(jié)束了檀夹,此時(shí)的 i 的值為 4筋粗,所以 function() { console.log(i) } 去找變量 i策橘,只能拿到 4。
我們想起上一個(gè)例子中亏狰,閉包使 a 變量的值被保存起來(lái)了役纹,那么這里我們也可以用閉包把 0 1 2 3 保存起來(lái)偶摔。
for (var i = 0; i < 4; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
}, 0)
})(i)
}
當(dāng) i=0 時(shí)暇唾,把 0 作為參數(shù)傳進(jìn)匿名函數(shù)中,此時(shí) function(i){} 此匿名函數(shù)中的 i 的值為 0辰斋,等到 setTimeout 執(zhí)行時(shí)順著外層去找 i策州,這時(shí)就能拿到 0。如此循環(huán)宫仗,就能拿到想要的 0 1 2 3够挂。
內(nèi)存管理
在閉包中調(diào)用局部變量,會(huì)導(dǎo)致這個(gè)局部變量無(wú)法及時(shí)被銷毀藕夫,相當(dāng)于全局變量一樣會(huì)一直占用著內(nèi)存孽糖。如果需要回收這些變量占用的內(nèi)存,可以手動(dòng)將變量設(shè)置為null毅贮。
然而在使用閉包的過(guò)程中办悟,比較容易形成 JavaScript 對(duì)象和 DOM 對(duì)象的循環(huán)引用,就有可能造成內(nèi)存泄露滩褥。這是因?yàn)闉g覽器的垃圾回收機(jī)制中病蛉,如果兩個(gè)對(duì)象之間形成了循環(huán)引用,那么它們都無(wú)法被回收瑰煎。
function func() {
var test = document.getElementById('test');
test.onclick = function () {
console.log('hello world');
}
}
在上面例子中铺然,func 函數(shù)中用匿名函數(shù)創(chuàng)建了一個(gè)閉包。變量 test 是 JavaScript 對(duì)象酒甸,引用了 id 為 test 的 DOM 對(duì)象魄健,DOM 對(duì)象的 onclick 屬性又引用了閉包,而閉包又可以調(diào)用 test 插勤,因而形成了循環(huán)引用沽瘦,導(dǎo)致兩個(gè)對(duì)象都無(wú)法被回收。要解決這個(gè)問(wèn)題饮六,只需要把循環(huán)引用中的變量設(shè)為 null 即可其垄。
function func() {
var test = document.getElementById('test');
test.onclick = function () {
console.log('hello world');
}
test = null;
}
如果在 func 函數(shù)中使用匿名函數(shù)創(chuàng)建閉包,而是通過(guò)引用一個(gè)外部函數(shù)卤橄,也不會(huì)出現(xiàn)循環(huán)引用的問(wèn)題绿满。
function func() {
var test = document.getElementById('test');
test.onclick = funcTest;
}
function funcTest(){
console.log('hello world');
}