前置知識
es6之前挤聘,js中變量作用域分為兩種:全局作用域凉敲、局部作用域衣盾。
學(xué)習(xí)閉包之前需要先了解作用域及變量提升的概念∫ィ《JS變量作用域&作用域鏈》势决,《js變量提升》
通過了解變量作用域我們知道,js的變量作用域很特殊蓝撇,采用的是“詞法作用域”徽龟。
子作用域可以訪問父作用域的變量。
但是父作用域無法訪問到子作用域的變量唉地。
調(diào)用棧:
我們在執(zhí)行一個函數(shù)時,如果這個函數(shù)又調(diào)用了另外一個函數(shù)传透,而這個“另外一個函數(shù)”也調(diào)用了“另外一個函數(shù)”耘沼,便形成了一系列的調(diào)用棧
function fn1() {
fn2()
}
function fn2() {
fn3()
}
function fn3() {
fn4()
}
function fn4() {
console.log('fn4')
}
fn1()
調(diào)用棧的原則是先進后出,后進先出朱盐。
fn1 先入棧群嗤,fn1 調(diào)用fn2,fn2 入棧兵琳,……狂秘,直到 fn4 執(zhí)行完成骇径,fn4 先出棧,fn3者春,fn2破衔,fn1 分別出棧。
正常來講钱烟,函數(shù)執(zhí)行完畢出棧時晰筛,函數(shù)內(nèi)局部變量會在下一個垃圾回收節(jié)點被回收,該函數(shù)對應(yīng)的執(zhí)行上下文會被銷毀拴袭。
重點:這也就是我們在外界無法訪問函數(shù)內(nèi)部定義的變量的原因读第。
也就是說,只有在函數(shù)執(zhí)行時拥刻,相關(guān)函數(shù)可以訪問該變量怜瞒,該變量在預(yù)編譯階段進行創(chuàng)建,在執(zhí)行階段進行激活般哼,在函數(shù)執(zhí)行完畢后吴汪,相關(guān)上下文被銷毀。
為何使用閉包
但是出于一些原因逝她,有時候我們需要得到函數(shù)內(nèi)部的局部變量浇坐,通過上面的解釋知道常規(guī)的手段是不行的,
那就使用非常規(guī)手段黔宛,就是讓無數(shù)人翻車的閉包近刘。
何為閉包
閉包的概念:閉包的概念也可以理解為函數(shù)的概念,即
函數(shù)對象可以通過作用域鏈關(guān)聯(lián)起來臀晃,函數(shù)體內(nèi)部的變量都可以保存在函數(shù)作用域內(nèi)觉渴,這種特性在計算機科學(xué)文獻中成為“閉包”。
這句話是犀牛書8.6節(jié)閉包中的一段定義徽惋,可能過于官方案淋,很多人都不太理解,那我們把這句話再翻譯一下:
一個函數(shù)內(nèi)部的函數(shù)可以訪問到外部函數(shù)的變量险绘。
再換句話說就是:
函數(shù)嵌套函數(shù)時踢京,內(nèi)層函數(shù)引用了外層函數(shù)作用域下的變量,并且內(nèi)層函數(shù)在全局環(huán)境下可訪問宦棺,就形成了閉包瓣距。
從技術(shù)角度來說,所有的JavaScript函數(shù)都是閉包代咸。
注:閉包函數(shù)內(nèi)不一定要有return蹈丸,如沒有 return 那么就要將一個內(nèi)部函數(shù)賦值給一個全局變量,否則沒有意義。當然也可以返回一個對象(見最后一個栗子)逻杖。
老司機來了奋岁,快上車
下面通過幾個栗子,讓大家快速了解閉包
- 栗子1
function fn(){
var a = 5;
}
a是函數(shù)fn的局部變量荸百,在外部是無法訪問到的闻伶,但是由于子作用域可以訪問父作用域的變量,我們將代碼簡單修改代碼↓
function fn(){
var a = 5;
function fn2(){
console.log(a);
}
}
在函數(shù)fn內(nèi)部定義函數(shù)fn2管搪,fn2內(nèi)部可以訪問到a變量虾攻,那是不是可以將函數(shù)fn2作為返回值,這樣是不是就可以在函數(shù)fn外部獲取到變量a了更鲁,再改寫代碼↓
function fn(){
var a=5;
return function(){
console.log(a);
}
}
var fn3 = fn();
fn3(); //5
將fn函數(shù)內(nèi)部函數(shù)作為返回值霎箍,然后在函數(shù)fn外部調(diào)用返回的函數(shù),正確輸出a澡为。這樣就實現(xiàn)了我們最開始的需求(在函數(shù)外部拿到函數(shù)的局部變量)漂坏。
為什么會這樣?
首先再復(fù)習(xí)一遍閉包定義媒至,“一個函數(shù)的內(nèi)部函數(shù)可以拿到外部函數(shù)的變量”顶别。
再具體一點就是:一個函數(shù)的內(nèi)部函數(shù)可以拿到外部函數(shù)的變量,然后將這個內(nèi)部函數(shù)作為返回值返回拒啰。
這樣在函數(shù)外部調(diào)用返回的函數(shù)時同樣可以拿到函數(shù)內(nèi)部的這個變量驯绎,這就是閉包。
什么原理谋旦?
一個普通的函數(shù)在執(zhí)行完后剩失,上下文即被銷毀,內(nèi)部的變量都會被釋放册着,但是這在個栗子中拴孤,js引擎發(fā)現(xiàn)返回的函數(shù)中使用了變量a,并且這個返回的函數(shù)在外部是有可能被執(zhí)行的甲捏,所以變量a沒有被釋放演熟,而是放到了一個只有這個返回的函數(shù)可以訪問到的地方,此時a變量可以且只能被這個函數(shù)訪問司顿,每次調(diào)用fn()都會創(chuàng)建一個新的作用域鏈和一個新的私有變量芒粹。
到這你還是有點懵,沒理解大溜,不用怕化漆,剛接觸都會懵,將上面的栗子反復(fù)看幾遍猎提,總會有所收獲的。
如果到這你都能看懂,那么恭喜你锨苏,你已經(jīng)掌握了閉包的基礎(chǔ)用法疙教。系好安全帶,開始飆車了伞租。
- 栗子2
function fn(){
var a = 1;
return function(){
a++;
console.log(a);
}
}
var fn2 = fn();
fn2(); //2
fn2(); //3
fn2(); //4
這里可以看到贞谓,我們不光可以獲取到fn函數(shù)內(nèi)的局部變量a,還可以對其進行修改葵诈。因為變量a是一直存放在內(nèi)存中fn2函數(shù)可以訪問到的地方裸弦。
再升級下代碼↓
- 栗子3
function fn() {
var a = 1;
return function() {
a ++;
console.log(a);
}
}
var fn1 = fn();
fn1(); //2
var fn2 = fn();
fn2(); //2
fn2(); //3
var fn3 = fn();
fn3(); //2
上面代碼將fn的返回函數(shù)分別賦給3個對象,fn1作喘、fn2理疙、fn3,
三次賦值相當于初始化3個a變量放到內(nèi)存中,分別只供fn1泞坦、fn2窖贤、fn3使用。
fn1贰锁、fn2赃梧、fn3函數(shù)在執(zhí)行的時候,分別訪問的是各自區(qū)域內(nèi)的a變量豌熄,3個區(qū)域不共享授嘀。
原理:每次調(diào)用fn()都會創(chuàng)建一個新的作用域鏈和一個新的私有變量。
- 栗子4
//第一題
function q1() {
var a = {};
ruturn function() {
return a;
}
}
var t1 = q1();
var o1 = t1();
var o2 = t1();
console.log(o1 == o2);//true
//第二題
function q2() {
var a = {};
ruturn function() {
return a;
}
}
var t1 = q2();
var t2 = q2();
var o1 = t1();
var o2 = t2();
console.log(o1 == o2);//false
分別輸出true和false锣险,不需要解釋了吧蹄皱。
- 栗子5
一些情況下,需要返回多個函數(shù)囱持,這時候就用到返回對象
function fn() {
var a = 10;
return {
add:function(addNum) {
a += addNum;
console.log(a);
},
sub:function(subNum) {
a -= subNum;
console.log(a);
}
}
}
var obj1 = fn();
obj1.add(5); // 15
obj1.add(20); // 35
obj1.sub(3); // 32
var obj2 = fn();
obj2.add(2); // 12
obj2.add(6); // 18
返回對象和返回函數(shù)用法基本一致夯接,變量在不同對象間依然不共享。
- 栗子6
const foo = () => {
var arr = []
var i
for (i = 0; i < 10; i++) {
arr[i] = function () {
console.log(i)
}
}
return arr[0]
}
foo()()
輸出10纷妆。
- 栗子7
var fn = null
const foo = () => {
var a = 2
function innerFoo() {
console.log(a)
}
fn = innerFoo
}
const bar = () => {
fn()
}
foo()
bar()
輸出2
在 foo 函數(shù)內(nèi)盔几,將 innerFoo 函數(shù)賦值給 fn,fn 是全局變量掩幢,這就導(dǎo)致了 foo 的變量對象 a 也被保留了下來逊拍。
這個栗子就說明了,閉包函數(shù)可以沒有顯式的 return 际邻。
- 栗子8
var fn = null
const foo = () => {
var a = 2
function innerFoo() {
console.log(c)
console.log(a)
}
fn = innerFoo
}
const bar = () => {
var c = 100
fn()
}
foo()
bar()
栗子8是栗子7的改版芯丧。
執(zhí)行結(jié)果為:報錯 ReferenceError: c is not defined。
變量 c 并不在其作用域鏈上世曾,c 只是 bar 函數(shù)的內(nèi)部變量缨恒。
說翻車就翻車——內(nèi)存管理
內(nèi)存管理就是:對內(nèi)存生命周期的管理。包含分配內(nèi)存空間、讀寫內(nèi)存骗露、釋放內(nèi)存空間岭佳。
var foo = 'bar' // 在棧內(nèi)存中給變量分配空間
alert(foo) // 使用內(nèi)存
foo = null // 釋放內(nèi)存空間
JavaScript依賴宿主瀏覽器的垃圾回收機制
如內(nèi)存管理不當極易造成內(nèi)存泄漏,指內(nèi)存空間明明已經(jīng)不再被使用萧锉,但由于某種原因并沒有被釋放的現(xiàn)象珊随。
- 栗子9
var element = document.getElementById('element')
element.innerHTML = '<button id="btn1">按鈕</button>'
var btn = document.getElementById('btn1')
btn.addEventListener('click', function () {
// ...
})
element.innerHTML = ''
栗子9中,button元素已經(jīng)從dom中移除柿隙,但是其事件處理句柄還在叶洞,所以依然無法被回收,需要手動removeEventListener禀崖。需要注意的是衩辟,addEventListener()添加的匿名函數(shù)無法移除,所以要盡量傳入具名函數(shù)帆焕。
另外閉包使用不當惭婿,極易造成內(nèi)存泄漏,如果不再使用叶雹,需要手動清除财饥。
之前說到閉包中的變量在函數(shù)執(zhí)行完后不會被釋放,還是存放在內(nèi)存中折晦,勢必會造成內(nèi)存浪了钥星。嚴重可導(dǎo)致內(nèi)存泄漏。
沒辦法直接釋放這個變量满着,如需釋放變量就釋放訪問變量的函數(shù)
- 栗子10
function foo() {
let a = 123
function bar() { alert(a) }
return bar
}
let bar = foo()
此時 a 變量會被保存在內(nèi)存中谦炒,如果需要釋放則執(zhí)行
bar = null
釋放掉對閉包函數(shù)的引用后,垃圾回收機制就會回收變量a风喇。
總結(jié)
很多人學(xué)完閉包都會有一個這樣的問題宁改,“我知道什么是閉包,可是閉包是做什么的呢魂莫?”
-
閉包的應(yīng)用場景
- 模塊化
- 防止變量被破壞
- Redux中間件實現(xiàn)機制
設(shè)計模式中的單例模式就可以依托閉包來實現(xiàn)
function Person() {}
const getSingleInstance = (function () {
var singleInstance
return function () {
if (singleInstance) {
return singleInstance
}
return singleInstance = new Person()
}
})()
const p1 = new getSingleInstance()
const p2 = new getSingleInstance()
console.log(p1 === p2) // true
singleInstance 為閉包變量 还蹲,這正是單例模式的體現(xiàn)。
一個閉包引申出了內(nèi)存耙考、執(zhí)行上下文阵赠、作用域就乓、作用域鏈等概念。雖說都是基礎(chǔ)柔吼,但是每個概念都能衍生出很多知識點践惑。難怪老司機也愛翻車哮独。