老司機也翻車的閉包

前置知識

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ǔ)柔吼,但是每個概念都能衍生出很多知識點践惑。難怪老司機也愛翻車哮独。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荚虚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子账蓉,更是在濱河造成了極大的恐慌,老刑警劉巖逾一,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剔猿,死亡現(xiàn)場離奇詭異,居然都是意外死亡嬉荆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門酷含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鄙早,“玉大人,你說我怎么就攤上這事椅亚∠薹” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵呀舔,是天一觀的道長弥虐。 經(jīng)常有香客問我,道長媚赖,這世上最難降的妖魔是什么霜瘪? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮惧磺,結(jié)果婚禮上颖对,老公的妹妹穿的比我還像新娘。我一直安慰自己磨隘,他們只是感情好缤底,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著番捂,像睡著了一般个唧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上设预,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天徙歼,我揣著相機與錄音,去河邊找鬼絮缅。 笑死鲁沥,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的耕魄。 我是一名探鬼主播画恰,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吸奴!你這毒婦竟也來了允扇?” 一聲冷哼從身側(cè)響起缠局,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎考润,沒想到半個月后狭园,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡糊治,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年唱矛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片井辜。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡绎谦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粥脚,到底是詐尸還是另有隱情窃肠,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布刷允,位于F島的核電站冤留,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏树灶。R本人自食惡果不足惜纤怒,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望天通。 院中可真熱鬧肪跋,春花似錦、人聲如沸土砂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萝映。三九已至吴叶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間序臂,已是汗流浹背蚌卤。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留奥秆,地道東北人逊彭。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像构订,于是被迫代替她去往敵國和親侮叮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容