寫在前面
閉包是JavaScript中一個(gè)重要的概念,本文使用3w(what拧略,why芦岂,how)原則總結(jié)一下閉包這個(gè)概念。
what垫蛆,什么是閉包禽最?
網(wǎng)上關(guān)于閉包的定義有很多腺怯,但大多過(guò)于繁雜。一種簡(jiǎn)潔的說(shuō)法:閉包是一個(gè)有狀態(tài)的函數(shù)(a stateful function)---- 首先川无,閉包是一個(gè)函數(shù)呛占,它一定存在于外層函數(shù)內(nèi);其次懦趋,閉包“記住了”其外層函數(shù)擁有的變量晾虑,它能夠訪問(wèn)外層函數(shù)的作用域仅叫。
下面的例子就形成了一個(gè)閉包:
function foo() {
var data = "hello";
function bar() {
console.log(data); // 打印 hello
}
return bar;
}
foo()();
函數(shù)bar就是一個(gè)閉包帜篇,它能夠訪問(wèn)外層函數(shù)foo的變量data。當(dāng)調(diào)用foo()()時(shí)诫咱,也就是調(diào)用了bar函數(shù)笙隙,將打印foo函數(shù)內(nèi)部變量data的值。
why坎缭,為什么要用閉包竟痰,作用是什么?
閉包多用來(lái)提供“模塊化”掏呼,起到“封裝”代碼的目的凯亮。
由于外部執(zhí)行環(huán)境無(wú)法訪問(wèn)函數(shù)內(nèi)部的變量,但可以通過(guò)將閉包暴露出來(lái)的方式哄尔,使外部獲取對(duì)函數(shù)內(nèi)部變量的訪問(wèn)權(quán)假消。
閉包的這個(gè)用法,類似于C++岭接、Java等語(yǔ)言的公有函數(shù)(public)富拗,提供外界訪問(wèn)類的私有成員變量(private)的能力。
下面是一個(gè)閉包提供的例子:
function Person() {
var name = "Joe";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
}
var p = Person();
console.log(p.name); // 打印undefined
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike
Person函數(shù)的內(nèi)部變量name只存在于Person的函數(shù)作用域內(nèi)鸣戴,無(wú)法被外界訪問(wèn)到啃沪,所以打印undefined。
通過(guò)閉包getName和setName窄锅,可以達(dá)到訪問(wèn)變量name的目的创千。
在es6中終于有了類的概念,上面的代碼可以用類的方式改寫:
class Person {
constructor() {
this.name = 'Joe';
}
getName() {
return this.name;
}
setName(newName) {
this.name = newName;
}
}
var p = new Person();
console.log(p.name); // 打印Joe
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike
es6由于沒(méi)有private概念入偷,可以看到這里的name屬性其實(shí)是可以被外界直接調(diào)用到的追驴。
另外在回調(diào)函數(shù)(如callback,setTimeout)疏之、異步執(zhí)行的函數(shù)(如ajax請(qǐng)求)中殿雪,經(jīng)常看到閉包的身影锋爪。閉包一定程度上簡(jiǎn)化了代碼的寫法丙曙。
how爸业,閉包是如何形成的?
說(shuō)起閉包的底層機(jī)制亏镰,就首先要搞懂js語(yǔ)言中的執(zhí)行環(huán)境和作用域鏈的概念扯旷,這篇文章介紹了這些概念。
閉包的原理其實(shí)就是形成閉包的這個(gè)內(nèi)層函數(shù)索抓,在其自身的作用域鏈上钧忽,頭結(jié)點(diǎn)(活動(dòng)對(duì)象)是自身的變量對(duì)象,記錄了自身內(nèi)部的變量信息纸兔;而作用域鏈上的第二個(gè)節(jié)點(diǎn)是其外層函數(shù)的變量對(duì)象,記錄了外層函數(shù)的變量信息----很顯然否副,這個(gè)節(jié)點(diǎn)上保存的信息能夠被閉包函數(shù)訪問(wèn)的到汉矿,閉包定義所說(shuō)的“周圍環(huán)境”,其實(shí)就是這個(gè)數(shù)據(jù)备禀。
一般的洲拇,當(dāng)外層函數(shù)執(zhí)行完成后,js引擎會(huì)釋放掉它的執(zhí)行環(huán)境曲尸,作用域鏈等信息赋续。但如果其內(nèi)部含有閉包函數(shù),由于閉包函數(shù)的作用域鏈上仍然引用著外層函數(shù)的變量對(duì)象(第二個(gè)節(jié)點(diǎn))另患,垃圾回收器判定這塊內(nèi)存仍然有在被使用(被閉包引用)纽乱,就不會(huì)釋放掉這塊內(nèi)存,所以即使外層函數(shù)執(zhí)行完昆箕,我們依然能通過(guò)閉包函數(shù)鸦列,訪問(wèn)其外層函數(shù)的變量信息。經(jīng)常有說(shuō)閉包容易引起內(nèi)存泄漏鹏倘,也就是因?yàn)閮?nèi)存一直被閉包引用者薯嗤,無(wú)法被垃圾回收導(dǎo)致的結(jié)果。
閉包的這種底層實(shí)現(xiàn)纤泵,有時(shí)候會(huì)引起看上去有點(diǎn)預(yù)想不到的結(jié)果骆姐,比如在for循環(huán)中。思考下面的例子捏题,為什么打印出來(lái)的都是5呢玻褪?
function arrayFun() {
var arr = [];
for (var i = 0; i < 5; i++) {
arr.push(function () {
console.log(i);
});
}
return arr;
}
var retArr = arrayFun();
for (var i = 0; i < 5; i++) {
retArr[i](); // 打印 5,5公荧,5归园,5,5
}
由于在es6之前稚矿,只有全局作用域和函數(shù)作用域庸诱,i變量在函數(shù)arrayFun中捻浦,只有一份副本。那么閉包(此處是個(gè)匿名函數(shù))在作用域鏈上引用的是同一個(gè)i值(i在執(zhí)行完for循環(huán)后值變?yōu)?)桥爽,當(dāng)調(diào)用閉包時(shí)朱灿,訪問(wèn)作用域鏈上的i值,就都是5了钠四。
在es6中引入了let和塊作用域盗扒,i的值在塊作用域中是不同的副本,所以就不會(huì)出現(xiàn)上面的情況了缀去。仍然是這篇文章有代碼實(shí)例侣灶,不再贅述。