1.什么是閉包吱瘩?
要了解什么是閉包宛官,首先你要了解作用域。
js的作用域分兩種凶杖,全局作用域和局部作用域胁艰。
我們知道在js作用域環(huán)境中訪問變量的順序是由內(nèi)向外的,內(nèi)部作用域可以獲得當(dāng)前作用域下的變量和當(dāng)前作用域的外層作用域下的變量智蝠,反之則不能腾么,也就是說在外層作用域下無法獲取內(nèi)層作用域下的變量,同樣在不同的函數(shù)作用域中也是不能相互訪問彼此變量的杈湾。
那么我們想在一個函數(shù)內(nèi)部也有限權(quán)訪問另一個函數(shù)內(nèi)部的變量該怎么辦呢解虱?
閉包就是用來解決這一需求的,閉包的本質(zhì)就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù)漆撞。
簡單講殴泰,閉包就是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。
2.閉包的特性
- 函數(shù)嵌套函數(shù)
- 函數(shù)內(nèi)部可以引用函數(shù)外部的參數(shù)和變量
- 參數(shù)和變量不會被垃圾回收機(jī)制回收
3.閉包解析
1.簡單的閉包案例
function fn1() {
var name = 'lisi';
function fn2() {
console.log(name);
}
fn2();
}
fn1();
因為fn2函數(shù)執(zhí)行時浮驳,會向外層找變量name悍汛,訪問到fn1的變量name,fn2本身是個函數(shù)至会,滿足’fn2函數(shù)訪問到fn1函數(shù)的變量‘离咐,所有由此產(chǎn)生了閉包
2.為什么閉包函數(shù)能夠訪問其他函數(shù)的作用域 ?
從堆棧的角度看待js函數(shù)
基本變量的值一般都是存在棧內(nèi)存中,而對象類型的變量的值存儲在堆內(nèi)存中奋献,棧內(nèi)存存儲對應(yīng)空間地址健霹⊥希基本的數(shù)據(jù)類型: Number 、Boolean糖埋、Undefined宣吱、String、Null瞳别。
js棧與堆詳情內(nèi)容請參考另一篇文章(js中的棧內(nèi)存與堆內(nèi)存)
// a 是一個對象
var a= {b: 10 }
當(dāng)我們執(zhí)行 a={b:20}時征候,堆內(nèi)存就會產(chǎn)生新的對象{b:20},棧內(nèi)存中a的指針會指向新的空間地址( {b:20} )祟敛,而堆內(nèi)存中原來的{b:10}就會被程序引擎垃圾回收掉疤坝,節(jié)約內(nèi)存空間。
我們知道js函數(shù)也是對象馆铁,它也是在堆與棧內(nèi)存中存儲的跑揉,我們來看一下轉(zhuǎn)化:
var a = 1;
function fn(){
var b = 2;
function fn1(){
console.log(b);
}
fn1();
}
fn();
由于棧是一種先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),所以全局執(zhí)行環(huán)境在最底層,我們由底自上看
- 在全局執(zhí)行環(huán)境(瀏覽器就是window作用域)埠巨,全局作用域里有個變量a及函數(shù)對象fn
- 進(jìn)入fn历谍,此時棧內(nèi)存就會開辟一個fn的執(zhí)行環(huán)境,這個環(huán)境里有變量b和函數(shù)對象fn1辣垒,這里可以訪問自身執(zhí)行環(huán)境和全局執(zhí)行環(huán)境所定義的變量a
- 進(jìn)入fn1望侈,此時棧內(nèi)存就會開辟 一個fn1的執(zhí)行環(huán)境,這里面沒有定義其他變量勋桶,但是我們可以訪問到fn和全局執(zhí)行環(huán)境里面的變量a脱衙,因為程序在訪問變量時,是向底層棧一個個找例驹,如果找到全局執(zhí)行環(huán)境里都沒有對應(yīng)變量捐韩,則程序拋出underfined的錯誤。
- 隨著fn1()執(zhí)行完畢眠饮,fn1的執(zhí)行環(huán)境被杯銷毀奥帘,接著執(zhí)行完fn()铜邮,fn的執(zhí)行環(huán)境也會被銷毀仪召,只剩全局的執(zhí)行環(huán)境下,現(xiàn)在沒有b變量松蒜,和fn1函數(shù)對象了扔茅,只有a 和 fn(函數(shù)聲明作用域是window下)
在函數(shù)內(nèi)訪問某個變量是根據(jù)函數(shù)作用域鏈來判斷變量是否存在的,而函數(shù)作用域鏈?zhǔn)浅绦蚋鶕?jù)函數(shù)所在的執(zhí)行環(huán)境棧來初始化的秸苗,所以上面的例子召娜,我們在fn1里面打印變量b,根據(jù)fn1的作用域鏈的找到對應(yīng)fn執(zhí)行環(huán)境下的變量b惊楼。所以當(dāng)程序在調(diào)用某個函數(shù)時玖瘸,做了一下的工作:準(zhǔn)備執(zhí)行環(huán)境秸讹,初始函數(shù)作用域鏈和arguments參數(shù)對象
3.閉包應(yīng)用
1.定時器與for循環(huán)
for (var i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
在這段代碼中,我們對其的預(yù)期是輸出1~10雅倒,但卻輸出10次11璃诀。這是因為 i 是聲明在全局作用中的,定時器中的匿名函數(shù)也是執(zhí)行在全局作用域中蔑匣,當(dāng)循環(huán)結(jié)束時劣欢,i 已經(jīng)是 11 了,所以每次都輸出11了裁良。
怎么解決這個問題呢凿将?
// 解決方法1: 使用es6語法,let聲明i
for (let i = 1; i <= 10; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
// 解決方法2:立即執(zhí)行函數(shù)IIFE
// 我們可以讓i在每次迭代的時候价脾,都產(chǎn)生一個私有的作用域牧抵,在這個私有的作用域中保存當(dāng)前i的值。
for (var i = 1; i <= 10; i++) {
(function () {
var j = i;
setTimeout(function () {
console.log(j);
}, 1000);
})();
}
2.this指向問題
var object = {
name: "object",
getName: function() {
return function() {
console.info(this.name)
}
}
}
object.getName()() // underfined
// 因為里面的閉包函數(shù)是在window作用域下執(zhí)行的侨把,也就是說灭忠,this指向windows
3.內(nèi)存泄露
function showId() {
var el = document.getElementById("app")
el.onclick = function(){
aler(el.id) // 這樣會導(dǎo)致閉包引用外層的el,當(dāng)執(zhí)行完showId后座硕,el無法釋放
}
}
// 改成下面
function showId() {
var el = document.getElementById("app")
var id = el.id
el.onclick = function(){
aler(id) // 這樣會導(dǎo)致閉包引用外層的el弛作,當(dāng)執(zhí)行完showId后,el無法釋放
}
el = null // 主動釋放el
}
4.遞歸調(diào)用
function factorial(num) {
if(num<= 1) {
return 1;
} else {
return num * factorial(num-1)
}
}
var anotherFactorial = factorial
factorial = null
anotherFactorial(4) // 報錯 华匾,因為最好是return num* arguments.callee(num-1)映琳,arguments.callee指向當(dāng)前執(zhí)行函數(shù),但是在嚴(yán)格模式下不能使用該屬性也會報錯蜘拉,所以借助閉包來實現(xiàn)
// 使用閉包實現(xiàn)遞歸
function newFactorial = (function f(num){
if(num<1) {return 1}
else {
return num* f(num-1)
}
}) //這樣就沒有問題了萨西,實際上起作用的是閉包函數(shù)f,而不是外面的函數(shù)newFactorial
4.閉包的好與壞
好處
- 保護(hù)函數(shù)內(nèi)的變量安全 旭旭,實現(xiàn)封裝谎脯,防止變量流入其他環(huán)境發(fā)生命名沖突
- 在內(nèi)存中維持一個變量,可以做緩存(但使用多了同時也是一項缺點持寄,消耗內(nèi)存)
- 匿名自執(zhí)行函數(shù)可以減少內(nèi)存消耗
壞處
- 其中一點上面已經(jīng)有體現(xiàn)了源梭,就是被引用的私有變量不能被銷毀,增大了內(nèi)存消耗稍味,造成內(nèi)存泄漏废麻,解決方法是可以在使用完變量后手動為它賦值為null;
- 其次由于閉包涉及跨域訪問模庐,所以會導(dǎo)致性能損失烛愧,我們可以通過把跨作用域變量存儲在局部變量中,然后直接訪問局部變量,來減輕對執(zhí)行速度的影響
如果不是因為某些特殊任務(wù)而需要閉包怜姿,在沒有必要的情況下慎冤,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包對腳本性能具有負(fù)面影響沧卢,包括處理速度和內(nèi)存消耗粪薛。
5.閉包例題檢驗:
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?
undefined
0
0
0
undefined, 0, 1, 2
undefined, 0
1
1