一文搞定閉包原理

前言

閉包是JS中重要的內(nèi)容洪鸭,對大多數(shù)人來說都會(huì)覺的閉包本身很好理解民珍,不就是一個(gè)函數(shù)嵌套一個(gè)函數(shù)嗎?但是再深入解釋時(shí)放航,好像不知道要說些啥烈拒。不用擔(dān)心,相信看完這篇你對閉包的理解就不僅僅只停留在概念層面上了三椿。

基本概念

1缺菌、閉包是什么?

官方對閉包的解釋是:一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個(gè)函數(shù))搜锰,因而這些變量也是該表達(dá)式的一部分。

通俗的解釋是:閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)耿战。

更清晰的講:閉包就是一個(gè)函數(shù)蛋叼,這個(gè)函數(shù)能夠訪問其他函數(shù)的作用域中的變量。

JS為什么使用閉包剂陡?

因?yàn)?code>JS中變量的作用域分為全局變量和局部變量狈涮。在函數(shù)外部無法讀取函數(shù)內(nèi)的局部變量。需要閉包來解決鸭栖。

閉包帶來的問題歌馍?

濫用閉包,會(huì)造成內(nèi)存泄漏晕鹊;由于閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中松却,內(nèi)存消耗很大暴浦,所以不能濫用閉包,否則會(huì)造成網(wǎng)頁的性能問題晓锻,在IE中可能導(dǎo)致內(nèi)存泄露歌焦。解決方法是,在退出函數(shù)之前砚哆,將不使用的局部變量指向null独撇。

閉包相關(guān)概念

在了解閉包之前,先來了解作用域躁锁、執(zhí)行上下文纷铣、變量對象、活動(dòng)對象战转、作用域鏈关炼,這些將有助于對閉包的理解。

作用域(Scope)

作用域是一套規(guī)則匣吊,用于確定在何處以及如何查找變量(標(biāo)識符)儒拂。

1、作用域分為

  • 全局作用域
  • 函數(shù)作用域(ES6新增了塊級作用域)

PS:這些相信很多人都知道色鸳,就不詳細(xì)舉例了

2社痛、作用域共有兩種主要的工作模型

  • 詞法(靜態(tài))作用域:作用域是在編寫代碼的時(shí)候確定的
  • 動(dòng)態(tài)作用域:作用域是在代碼運(yùn)行的時(shí)候確定的

我們知道Javascript使用的是詞法(靜態(tài))作用域

3命雀、詞法(靜態(tài))作用域

理解靜態(tài)作用域之前蒜哀,首先要先了解Js在編譯階段做了些什么事情。

Js編譯階段分為三個(gè)階段吏砂,下面概括一下三個(gè)階段:

1. 分詞/詞法分析(Tokenizing/Lexing)

其實(shí)我們寫的代碼就是字符串撵儿,在編譯的第一個(gè)階段里,把這些字符串轉(zhuǎn)成詞法單元(toekn)狐血。

2. 解析/語法分析(Parsing)

在有了詞法單元之后淀歇,JS還需要繼續(xù)分解代碼中的語法以便為JS引擎減輕負(fù)擔(dān),通過詞法單元生成了一個(gè)抽象語法樹(Abstract Syntax Tree),它的作用是為JS引擎構(gòu)造出一份程序語法樹匈织,我們簡稱為AST浪默。

3. 代碼生成(raw code)

這個(gè)階段主要做的就是拿AST來生成一份JS語言內(nèi)部認(rèn)可的代碼。

靜態(tài)作用域是發(fā)生在編譯階段的第一個(gè)步驟當(dāng)中缀匕,也就是分詞/詞法分析階段纳决。它有兩種可能,分詞和詞法分析乡小,分詞是無狀態(tài)的阔加,而詞法分析是有狀態(tài)的。

那我們?nèi)绾闻袛嘤袩o狀態(tài)呢满钟?以var a = 1為例胜榔。

  • 如果詞法單元生成器在判斷a是否為一個(gè)獨(dú)立的詞法單元時(shí)胳喷,調(diào)用的是有狀態(tài)的解析規(guī)則(生成器不清楚它是否依賴于其他詞法單元,所以要進(jìn)一步解析)苗分。
  • 如果它不用生成器判斷厌蔽,是一條不用被賦予語意的代碼(暫時(shí)可以理解為不涉及作用域的代碼,因?yàn)閖s內(nèi)部定義什么樣的規(guī)則我們并不清楚),那就被列入分詞中了摔癣。

總的來說奴饮,如果詞法單元生成器拿不準(zhǔn)當(dāng)前詞法單元是否為獨(dú)立的,就進(jìn)入詞法分析择浊,否則就進(jìn)入分詞階段戴卜。

簡單的說,詞法作用域就是定義在詞法階段的作用域琢岩。詞法作用域就是你編寫代碼時(shí)投剥,變量和塊級作用域?qū)懺谀睦餂Q定的。當(dāng)詞法解析器處理代碼時(shí)担孔,會(huì)保持作用域不變(除動(dòng)態(tài)作用域)江锨。

執(zhí)行上下文(Execution Contexts)

1、Javascript中代碼的執(zhí)行上下文分為以下三種

  • 全局級別的代碼 – 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境糕篇,一旦代碼被載入啄育,引擎最先進(jìn)入的就是這個(gè)環(huán)境。
  • 函數(shù)級別的代碼 – 當(dāng)執(zhí)行一個(gè)函數(shù)時(shí)拌消,運(yùn)行函數(shù)體中的代碼挑豌。
  • Eval的代碼 – 在Eval函數(shù)內(nèi)運(yùn)行的代碼。

2墩崩、一個(gè)執(zhí)行的上下文可以抽象的理解為一個(gè)對象氓英。每一個(gè)執(zhí)行的上下文都有一系列的屬性:

  • 變量對象(variable object)
  • this指針(this value)
  • 作用域鏈(scope chain)

代碼表示如下:

Execution Contexts = {
    variable object:變量對象/活動(dòng)對象;
    this value: this指針;
    scope chain:作用域鏈;
}

3、如何管理執(zhí)行上下文

Js的運(yùn)行采用的方式對執(zhí)行上下文進(jìn)行管理鹦筹,棧底始終是全局上下文铝阐,棧頂始終是正在被調(diào)用執(zhí)行的函數(shù)的執(zhí)行上下文。

實(shí)例

var a = 10;
var b = 'hello';

function fun1 () {
    console.log('i am fun1...');
    fun2();
}

function fun2 () {
    console.log('i am fun2...');
}

fun1();

上面代碼具體流程是:

  1. 當(dāng)Js文件開始執(zhí)行時(shí)盛龄,創(chuàng)建全局上下文饰迹,并pushcall stack
  2. fun1()被調(diào)用時(shí)余舶,創(chuàng)建fun1上下文,pushcall stack锹淌。
  3. fun2()被調(diào)用時(shí)匿值,創(chuàng)建fun2上下文,pushcall stack赂摆。
  4. fun2()執(zhí)行完畢挟憔,fun2上下文pop出棧钟些,等待被回收。
  5. fun1()執(zhí)行完畢绊谭,fun1上下文pop出棧政恍,等待被回收。
  6. 全局執(zhí)行環(huán)境不會(huì)出棧达传。

4篙耗、執(zhí)行環(huán)境的生命周期

執(zhí)行上下文生命周期分為創(chuàng)建階段、執(zhí)行階段宪赶、執(zhí)行完畢宗弯。如下圖:

image

執(zhí)行上下文是代碼執(zhí)行的一種抽象,而代碼執(zhí)行除了整個(gè)Js開始執(zhí)行之外搂妻,代碼的執(zhí)行都是通過函數(shù)調(diào)用執(zhí)行的蒙保,所以執(zhí)行上下文生命周期的各個(gè)階段其實(shí)是可以分別對應(yīng)函數(shù)被調(diào)用時(shí)的初始化、執(zhí)行欲主、執(zhí)行完畢階段的邓厕。下面會(huì)詳細(xì)的解釋每個(gè)階段的過程。

變量對象(variable object)

1扁瓢、變量對象的定義:

如果變量與執(zhí)行上下文相關(guān)详恼,那變量自己應(yīng)該知道它的數(shù)據(jù)存儲(chǔ)在哪里,并且知道如何訪問涤妒。這種機(jī)制稱為變量對象(variable object)单雾。

2、變量對象的作用:

可以說變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域(scope of data) 她紫。它是與執(zhí)行上下文關(guān)聯(lián)的特殊對象硅堆,用于存儲(chǔ)被定義在執(zhí)行上下文中的變量(variables)、函數(shù)聲明(function declarations) 贿讹、arguments渐逃。

3、變量對象的創(chuàng)建過程:

實(shí)例

function add(num){
    var sum = 5;
    return sum + num;
}
var sum = add(4);

根據(jù)上面代碼民褂,創(chuàng)建變量對象的流程是:

  1. 檢查當(dāng)前執(zhí)行環(huán)境上的參數(shù)列表茄菊,建立Arguments對象,并作為add VOarguments屬性值赊堪。
  2. 檢查當(dāng)前執(zhí)行環(huán)境上的function函數(shù)聲明面殖,每檢查到一個(gè)函數(shù)聲明,就在變量對象中以函數(shù)名建立一個(gè)屬性哭廉,屬性指向函數(shù)所在的內(nèi)存地址脊僚。
  3. 檢查當(dāng)前執(zhí)行環(huán)境上的所有var變量聲明。每檢查到一個(gè)var聲明遵绰,如果VO中已存在function屬性名則跳過辽幌,如果沒有就在變量對象中以變量名新建一個(gè)屬性增淹,屬性值為undefined

當(dāng)進(jìn)入全局上下文時(shí)乌企,全局上下文的變量對象可表示為:

VO = {
    add: <reference to function>,
    sum: undefined,
    Math: <...>,
    String: <...>
    ...
    window: global //引用自身
}

活動(dòng)對象(Activation Object)

當(dāng)函數(shù)被調(diào)用者激活時(shí)虑润,這個(gè)特殊的活動(dòng)對象(activation object) 就被創(chuàng)建了。它包含普通參數(shù)(formal parameters) 與特殊參數(shù)(arguments)對象(具有索引屬性的參數(shù)映射表)加酵∪鳎活動(dòng)對象在函數(shù)上下文中作為變量對象使用。

image

根據(jù)上圖虽画,簡單解釋:在沒有執(zhí)行當(dāng)前環(huán)境之前舞蔽,變量對象中的屬性都不能訪問!但是進(jìn)入執(zhí)行階段之后码撰,變量對象轉(zhuǎn)變?yōu)榱嘶顒?dòng)對象渗柿,里面的屬性都能被訪問了,然后開始進(jìn)行執(zhí)行階段的操作脖岛。所以活動(dòng)對象實(shí)際就是變量對象在真正執(zhí)行時(shí)的另一種形式朵栖。

根據(jù)上面變量對象的實(shí)例。當(dāng)add函數(shù)被調(diào)用時(shí)柴梆,add函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端陨溅,add函數(shù)執(zhí)行上下文中活動(dòng)對象可表示為

AO = {
    num: 4,
    sum: 5,
    arguments:{0:4}
}

作用域鏈 (Scope Chain)

函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時(shí)創(chuàng)建的,包含活動(dòng)對象AO和這個(gè)函數(shù)內(nèi)部的[[scope]]屬性绍在。

實(shí)例

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); 

在這段代碼中我們看到變量y在函數(shù)foo中定義(意味著它在foo上下文的AO中)z在函數(shù)bar中定義门扇,但是變量x并未在bar上下文中定義,相應(yīng)地偿渡,它也不會(huì)添加到barAO中臼寄。乍一看,變量x相對于函數(shù)bar根本就不存在溜宽。

函數(shù)bar如何訪問到變量x吉拳?理論上函數(shù)應(yīng)該能訪問一個(gè)更高一層上下文的變量對象。實(shí)際上它正是這樣适揉,這種機(jī)制是通過函數(shù)內(nèi)部的[[scope]]屬性來實(shí)現(xiàn)的留攒。
[[scope]]是所有父級變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上嫉嘀,在函數(shù)創(chuàng)建時(shí)存于其中炼邀。

根據(jù)上面代碼我們逐步分析:

  1. 代碼初始化時(shí),創(chuàng)建全局上下文的變量對象剪侮。
globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};
  1. foo創(chuàng)建時(shí)汤善,foo[[scope]]屬性是:
foo.[[Scope]] = [
  globalContext.VO
];
  1. foo激活時(shí)(進(jìn)入上下文),foo上下文的活動(dòng)對象票彪。
fooContext.AO = {
  y: 20,
  bar: <reference to function>
};
  1. foo上下文的作用域鏈為:
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];
  1. 內(nèi)部函數(shù)bar創(chuàng)建時(shí)红淡,其[[scope]]為:
bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];
  1. bar激活時(shí),bar上下文的活動(dòng)對象為:
barContext.AO = {
  z: 30
};
  1. bar上下文的作用域鏈為:
bar.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

閉包的原理

了解了上面的相關(guān)概念之后降铸,我們通過一個(gè)閉包的例子來分析一下閉包的形成原理在旱。

function add(){
    var sum =5;
    var func = function () {
        console.log(sum);
    }
    return func;
}
var addFunc = add();
addFunc(); //5

根據(jù)上面代碼我們逐步分析:

  1. Js執(zhí)行流進(jìn)入全局執(zhí)行上下文環(huán)境時(shí),全局執(zhí)行上下文可表示為:
globalContext = {
    VO: {
        add: <reference to function>,
        addFunc: undefined
    },
    this: window,
    scope chain: window 
}
  1. 當(dāng)add函數(shù)被調(diào)用時(shí),add函數(shù)執(zhí)行上下文可表示為:
addContext = {
    AO: {
        sum: undefined //代碼進(jìn)入執(zhí)行階段時(shí)此處被賦值為5
        func: undefined //代碼進(jìn)入執(zhí)行階段時(shí)此處被賦值為function (){console.log(sum);}
    },
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}
  1. add函數(shù)執(zhí)行完畢后推掸,Js執(zhí)行流回到全局上下文環(huán)境中桶蝎,將add函數(shù)的返回值賦值給addFunc

  2. 由于addFunc仍保存著func函數(shù)的引用谅畅,所以add函數(shù)執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內(nèi)存中登渣。

  3. 當(dāng)addFunc()執(zhí)行時(shí),func函數(shù)被調(diào)用毡泻,此時(shí)func函數(shù)執(zhí)行上下文可表示為:

funcContext = {
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}

當(dāng)要訪問變量sum時(shí)胜茧,func的活動(dòng)對象中未能找到,則會(huì)沿著作用域鏈查找仇味,由于Js遵循詞法作用域呻顽,作用域在函數(shù)創(chuàng)建階段就被確定,在add函數(shù)的活動(dòng)對象中找到sum = 5;

閉包的用法實(shí)戰(zhàn)

閉包可以用在許多地方丹墨。它的最大用處有兩個(gè)廊遍,一個(gè)是可以讀取函數(shù)內(nèi)部的變量,另一個(gè)就是讓這些變量的值始終保持在內(nèi)存中贩挣。本文閉包的用法不是重點(diǎn)內(nèi)容喉前,如果想了解更多方法,可以自行查閱資料王财。下面列舉幾個(gè)應(yīng)用方法卵迂。

1、延遲回調(diào)

var a = 10;
setTimeout(function () {
  alert(a); // 10, after one second
}, 1000);

2搪搏、回調(diào)函數(shù)

//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
  // 當(dāng)數(shù)據(jù)就緒的時(shí)候狭握,才會(huì)調(diào)用;
  // 這里,不論是在哪個(gè)上下文中創(chuàng)建
  // 此時(shí)變量“x”的值已經(jīng)存在了
  alert(x); // 10
};
//...

3疯溺、創(chuàng)建封裝的作用域來隱藏輔助對象

var foo = {};

// 初始化
(function (object) {

  var x = 10;

  object.getX = function _getX() {
    return x;
  };

})(foo);

alert(foo.getX()); // 獲得閉包 "x" – 10

總結(jié)

本文介紹了關(guān)于閉包以及閉包相關(guān)的知識论颅,如果對你有用,歡迎點(diǎn)贊收藏4涯邸J逊琛!??

相關(guān)文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末墨闲,一起剝皮案震驚了整個(gè)濱河市今妄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖盾鳞,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犬性,死亡現(xiàn)場離奇詭異,居然都是意外死亡腾仅,警方通過查閱死者的電腦和手機(jī)乒裆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來推励,“玉大人鹤耍,你說我怎么就攤上這事⊙榇牵” “怎么了稿黄?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長跌造。 經(jīng)常有香客問我杆怕,道長,這世上最難降的妖魔是什么鼻听? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任财著,我火速辦了婚禮,結(jié)果婚禮上撑碴,老公的妹妹穿的比我還像新娘撑教。我一直安慰自己,他們只是感情好醉拓,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布伟姐。 她就那樣靜靜地躺著,像睡著了一般亿卤。 火紅的嫁衣襯著肌膚如雪愤兵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天排吴,我揣著相機(jī)與錄音秆乳,去河邊找鬼。 笑死钻哩,一個(gè)胖子當(dāng)著我的面吹牛屹堰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播街氢,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼扯键,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了珊肃?” 一聲冷哼從身側(cè)響起荣刑,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤馅笙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后厉亏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體董习,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年叶堆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阱飘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,625評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虱颗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蔗喂,到底是詐尸還是另有隱情忘渔,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布缰儿,位于F島的核電站畦粮,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乖阵。R本人自食惡果不足惜宣赔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞪浸。 院中可真熱鬧儒将,春花似錦、人聲如沸对蒲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹈矮。三九已至砰逻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間泛鸟,已是汗流浹背蝠咆。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留北滥,地道東北人刚操。 一個(gè)月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像碑韵,于是被迫代替她去往敵國和親赡茸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評論 2 348

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