什么是閉包?
閉包的概念:
《JavaScript》權(quán)威指南: 函數(shù)對(duì)象可以通過(guò)作用域鏈相互關(guān)聯(lián)起來(lái),函數(shù)體內(nèi)部的變量可以保存在函數(shù)作用域內(nèi),這種特性稱為“閉包”。
通俗的說(shuō):所謂閉包,就是一個(gè)函數(shù),這個(gè)函數(shù)能夠訪問(wèn)其他函數(shù)作用域中的變量。
或者說(shuō):閉包,有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù);一般情況就是在一個(gè)函數(shù)中包含另一個(gè)函數(shù)(被包含的函數(shù)就是閉包)。
函數(shù)的作用域是獨(dú)立的、封閉的,外部的執(zhí)行環(huán)境是訪問(wèn)不了的,但是閉包具有這個(gè)能力和權(quán)限。
那閉包是怎樣的一個(gè)表現(xiàn)形式呢?
第一,閉包是一個(gè)函數(shù),而且存在于另一個(gè)函數(shù)當(dāng)中
第二,閉包是可以訪問(wèn)到父級(jí)函數(shù)的變量,且該變量不會(huì)銷毀
function person () {
var name = '小櫻'
function cat() { // 這個(gè)是一個(gè)內(nèi)部的函數(shù),是一個(gè)閉包
console.log(name)
}
return cat
}
var per = person() // per 的值就是return后的結(jié)果,即cat函數(shù)
per(); // 結(jié)果: 小櫻,per()就相當(dāng)于cat()
per(); // 小櫻
per(); // 小櫻
閉包的原理
閉包的實(shí)現(xiàn)原理,其實(shí)是利用了作用域鏈的特性,我們都知道作用域鏈就是在當(dāng)前執(zhí)行環(huán)境下訪問(wèn)某個(gè)變量時(shí),如果不存在就一直向外層尋找,最終尋找到最外層也就是全局作用域,這樣就形成了一個(gè)鏈條。
例如:
var age = 18
function cat () {
age++
console.log(age) // cat函數(shù)內(nèi)輸出了age,該作用域沒有,則向外層尋找,找到了,輸出19
}
cat() // 19
這個(gè)時(shí)候如果繼續(xù)調(diào)用,結(jié)果就會(huì)一直增加,也就是變量age的值一直在遞增
cat() // 20
cat() // 21
cat() // 22
如果程序還有其他函數(shù),也需要用到age的值,則會(huì)受到影響,而且全局變量還容易被人修改,比較不安全,這就是全局變量容易污染的原因,所以我們必須解決變量污染問(wèn)題,那就是把變量封裝到函數(shù)內(nèi)部,讓它成為局部變量。
如下:
// f2 加括號(hào)表示返回的是函數(shù)值,不加括號(hào)返回的是函數(shù)體
// 示例一
function f1() {
var age = 18
function f2() { // 是一個(gè)內(nèi)部函數(shù),是一個(gè)閉包
age++
console.log('年齡-->', age)
}
return f2();
}
f1() // 19
f1() // 19
// 示例二
function f1() {
var age = 18
function f2() { // 是一個(gè)內(nèi)部函數(shù),是一個(gè)閉包
age++
console.log('年齡-->', age)
}
return f2;
}
var a = f1()
a() // 19
a() // 20
示例一:內(nèi)部函數(shù) f2() 在執(zhí)行前,從外部函數(shù)返回。
示例二:f1( ) 執(zhí)行后,將其返回值(也就是內(nèi)部的 f2() 函數(shù))賦值給變量a, 并調(diào)用a(),實(shí)際上只是通過(guò)不同的標(biāo)示符引用調(diào)用了內(nèi)部的函數(shù)f2。
f1() 函數(shù)執(zhí)行后,正常情況下f1() 的整個(gè)內(nèi)部作用域被銷毀,占用的內(nèi)存被回收。但是現(xiàn)在的f1() 的內(nèi)部作用域f2() 還在使用,所以不會(huì)對(duì)其進(jìn)行回收。f2() 依然持有對(duì)改作用域的引用,這個(gè)引用叫做閉包。這個(gè)函數(shù)在定義的詞法作用域以外的地方被調(diào)用。閉包使得函數(shù)可以繼續(xù)訪問(wèn)定義時(shí)的詞法作用域。
常見的閉包
function foo(a) {
setTimeout(function timer() {
console.log(a)
}, 1000)
}
foo(2)
foo執(zhí)行1000ms后,它的內(nèi)部作用域不會(huì)消失,timer 函數(shù)依然保有foo 作用域的引用。timer函數(shù)就是一個(gè)閉包。
定時(shí)器,事件監(jiān)聽器,Ajax請(qǐng)求,跨窗口通信,Web Workers 或者其他異步或同步任務(wù)中,只要使用回調(diào)函數(shù),實(shí)際上就是閉包。
循環(huán)和閉包
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
上面的這段代碼,預(yù)期是每隔一秒,分別輸出0,1,2,3,4,但實(shí)際上依次輸出的是5,5,5,5,5。首先解釋一下5從哪里來(lái)的,這個(gè)循環(huán)的終止條件是 i 不在 < 5,條件首次成立時(shí) i 的值是5,因此,輸出顯示的是循環(huán)結(jié)束時(shí) i 的最終值。
延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。事實(shí)上,當(dāng)定時(shí)器運(yùn)行時(shí)即使每個(gè)迭代中執(zhí)行的都是setTimeout(..., 0) ,所有的回調(diào)函數(shù)依然是在循環(huán)結(jié)束后才執(zhí)行。因此每次都輸出一個(gè)5來(lái)。
我們預(yù)期的是每個(gè)迭代在運(yùn)行時(shí)都會(huì)給自己“捕獲”一個(gè) i 的的副本。但實(shí)際上,根據(jù)作用域的原理,盡管循環(huán)中的五個(gè)函數(shù)都是在各自迭代中分別定義的,但是他們都封閉在一個(gè)共享的全局作用域中,因此實(shí)際上只有一個(gè) i。即所有函數(shù)共享一個(gè) i 的引用。
改成下面這樣,就可以按照我們期望的方式進(jìn)行工作了。這樣修改之后,在每次迭代內(nèi)使用 IIFE (立即執(zhí)行函數(shù))會(huì)為每個(gè)迭代都生成一個(gè)新的作用域,使得延遲函數(shù)的回調(diào)可以將新的作用域封閉在每個(gè)迭代內(nèi)部,每個(gè)迭代內(nèi)部都會(huì)含有一個(gè)具有正確值的變量可以訪問(wèn)。
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
當(dāng)然,使用ES6 塊級(jí)作用域的 let 替換 var 也可以達(dá)到我們的目的。
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
閉包的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 隱藏變量,避免全局污染
- 可以讀取函數(shù)內(nèi)部的變量
使用不當(dāng),優(yōu)點(diǎn)就變成了缺點(diǎn): - 導(dǎo)致變量不會(huì)被垃圾回收機(jī)制回收,造成內(nèi)存消耗
- 不恰當(dāng)?shù)氖褂瞄]包可能會(huì)造成內(nèi)存泄漏的問(wèn)題。
為什么使用閉包時(shí)變量不會(huì)被垃圾回收機(jī)制銷毀呢?
JS垃圾回收機(jī)制:
JS規(guī)定在一個(gè)函數(shù)作用域內(nèi),程序執(zhí)行完以后變量就會(huì)被銷毀,這樣可以節(jié)省內(nèi)存;使用閉包時(shí),按照作用域鏈的特點(diǎn),閉包(函數(shù))外面的變量不會(huì)被銷毀,因?yàn)楹瘮?shù)會(huì)一直被調(diào)用,所有一直存在,如果閉包使用過(guò)多會(huì)造成內(nèi)存泄漏。
理解閉包
理解閉包首先要了解嵌套函數(shù)的詞法作用域規(guī)則,如下代碼
// 示例一
var str = 'Hello World' // 全局變量
var a = function () {
var str = 'Hello 你好' // 局部變量
function f() {
return str
}
return f()
}
console.log('示例1 ---->', a()) // 打印結(jié)果: Hello 你好
a () 函數(shù)聲明了一個(gè)局部變量,并定義了一個(gè)新的函數(shù)f(),函數(shù)f()返回了這個(gè)變量的值,最后將函數(shù)f()的執(zhí)行結(jié)果返回。
變量提升
變量提升即將變量聲明提升到它所在作用域的最開始的地方。
只有 var 可以將變量進(jìn)行提升,const 跟 let 不會(huì)
下面舉個(gè)例子:
變量提升(全局作用域)
// 示例1
console.log(a) // undefined
var a = 8
// 示例1 等價(jià)于
var a;
console.log(a) // undefined
a = 8
// 示例2
var a = 8;
function fn() {
console.log('1--->', a); // undefined
var a = 9;
console.log('2--->', a); // 9
}
fn()
// 示例2 等價(jià)于
var a = 8
function fn() {
var a;
console.log('1---->', a) // undefined
a = 9;
console.log('2---->', a) // 9
}