深入理解JavaScript中的閉包

閉包沒有想象的那么簡單

閉包的概念在JavaScript中占據(jù)了十分重要的地位弃衍,有不少開發(fā)者分不清匿名函數(shù)和閉包的概念敬锐,把它們混為一談它掂,我希望借這篇文章能夠讓大家對閉包有一個(gè)清晰的認(rèn)識琉用。

大家都知道變量的作用域有兩種:全局變量和局部變量落包。在JavaScript中函數(shù)內(nèi)部可以訪問外部全局變量焊傅,而函數(shù)外部無法訪問函數(shù)的內(nèi)部局部變量剂陡。

上邊這一小段話狈涮,看似簡單,其實(shí)它是我們理解閉包最基礎(chǔ)的東西鸭栖。在下邊的內(nèi)容中歌馍,我們會對這一現(xiàn)象做出解釋。我們先來看一個(gè)很簡單的例子:

const a = 100;

function f1() {
    console.log(a); // => 100
}

f1();

上邊的代碼中的函數(shù)f1打印出了全局變量a的值纤泵,這說明函數(shù)內(nèi)部可以訪問外部全局變量骆姐。出于某種目的,或者是為了安全捏题,或者是想使用私有變量玻褪,我們現(xiàn)在需要訪問函數(shù)內(nèi)部的一個(gè)局部變量。我們先看看下邊的代碼:

function f1() {
    const  a = 100;
    console.log(a); // => 100
}

console.log(a);

上邊的代碼會產(chǎn)生一個(gè)錯(cuò)誤公荧,說明我們無法在函數(shù)外部訪問函數(shù)內(nèi)部的局部變量带射。為了解決這個(gè)問題,我們就引出了閉包循狰,看一個(gè)使用閉包解決上述問題的例子:

function f1() {
    const  a = 100;
    return function () {
        console.log(a);
    }
}

f1()();

上邊的代碼是一個(gè)很簡答的例子窟社,使用閉包后我們打印出的結(jié)果是100.這正好驗(yàn)證了上邊說的,使用閉包的目的就是解決函數(shù)外部無法訪問函數(shù)內(nèi)部局部變量這一問題绪钥。

要徹底搞清楚其中的細(xì)節(jié)灿里,必須從理解函數(shù)第一次被調(diào)用的時(shí)候都會發(fā)生什么入手。

當(dāng)某個(gè)函數(shù)第一次被調(diào)用時(shí)程腹,會創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)和相應(yīng)的作用域鏈匣吊,并把作用域鏈賦值給一個(gè)特殊的內(nèi)部屬性(Scope),然后使用this寸潦,arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象(activation object)色鸳。但在作用域鏈中,外部函數(shù)的活動對象始終處于第二位见转,外部函數(shù)的外部函數(shù)的活動對象處于第三位命雀,直到作為作用域鏈重點(diǎn)的全局執(zhí)行環(huán)境。

上邊的這一段話非常重要斩箫,它解釋了函數(shù)執(zhí)行的基本原理吏砂,閉包也是函數(shù)。大家有可能對上邊的話不太理解校焦。我們通過一個(gè)例子來解釋一下:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

上邊的代碼中赊抖,我們首先定義了一個(gè)compare()函數(shù),然后又在全局作用域中調(diào)用了它寨典。當(dāng)?shù)谝淮握{(diào)用它的時(shí)候氛雪,一共創(chuàng)建了以下幾個(gè)對象:

  • 創(chuàng)建函數(shù)的執(zhí)行環(huán)境,當(dāng)然該函數(shù)的執(zhí)行環(huán)境是全局環(huán)境
  • 創(chuàng)建函數(shù)的作用域鏈
  • 創(chuàng)建一個(gè)包含this耸成,arguments报亩,value1浴鸿,value2的活動對象

我們看一個(gè)圖:

后臺的每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對象---變量對象,全局環(huán)境的變量對象始終存在弦追,而想compare()函數(shù)這樣的局部環(huán)境的變量對象岳链,則只在函數(shù)執(zhí)行的過程中存在。

變量對象本質(zhì)上是一個(gè)對象劲件,他存儲了某些變量值掸哑。

其實(shí),在compare()函數(shù)創(chuàng)建的時(shí)候零远,就已經(jīng)創(chuàng)建了一個(gè)預(yù)先包含全局變量對象的作用域鏈苗分,這個(gè)作用域鏈被保存在內(nèi)部的Scope屬性中。

這句話說明函數(shù)在創(chuàng)建后牵辣,其內(nèi)部就有了一個(gè)屬性保存著當(dāng)前的作用域鏈摔癣,上邊說compare()函數(shù)的作用域鏈指向全局變量對象,這說明作用域鏈中的每一項(xiàng)指向的都是一個(gè)變量對象纬向。

當(dāng)調(diào)用compare()函數(shù)時(shí)择浊,會為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境,然后通過復(fù)制函數(shù)的Scope屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈逾条。

這句話說明函數(shù)在執(zhí)行環(huán)境中琢岩,會新創(chuàng)建一個(gè)作用域鏈,這個(gè)新建的作用域鏈會把函數(shù)創(chuàng)建時(shí)的作用域鏈復(fù)制過來师脂。

此后粘捎,又有一個(gè)活動對象(在此作為變量對象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前段。

這句話說明危彩,函數(shù)執(zhí)行后,會他this泳桦,arguments汤徽,函數(shù)的參數(shù),函數(shù)內(nèi)部的局部變量這四個(gè)作為屬性灸撰,保存到一個(gè)對象中谒府,然后把該對象放到作用域鏈的前段,因此我們就能夠通過作用域鏈訪問到我們需要的數(shù)據(jù)浮毯。

大家可以再次回到上邊看看那個(gè)圖完疫,由于compare()函數(shù)是在全局環(huán)境中創(chuàng)建的,因此在執(zhí)行的時(shí)候债蓝,它的作用域鏈只有兩個(gè)對象壳鹤,最前端的0指向了執(zhí)行時(shí)的活動對象,1指向了全局的變量對象饰迹。

顯然芳誓,作用域鏈本質(zhì)上是一個(gè)指向變量對象的指針列表余舶,它只引用但不實(shí)際包含變量對象。

這句話非常重要锹淌,這使我們理解函數(shù)調(diào)用過程最基本的原理匿值。無論什么時(shí)候在函數(shù)中訪問一個(gè)變量時(shí),就會從作用域鏈中搜索具有相應(yīng)名字的變量赂摆。一般來講挟憔,當(dāng)函數(shù)執(zhí)行完畢后,局部活動對象就會被銷毀烟号,內(nèi)存中僅保存全局作用域(也就是全局執(zhí)行環(huán)境的變量對象)绊谭。但是閉包的情況又有所不同。

我們再看一個(gè)帶有閉包的例子:

function createCompareFunction(propertyName) {
    return function (object1, object2) {
        const value1 = object1[propertyName];
        const value2 = object2[propertyName];
        if (value1 < value2) {
            return -1;
        } else if (value1 > value2) {
            return 1;
        } else  {
            return 0;
        }
    }
}

const compare = createCompareFunction("name");
const result = compare({name: "James"}, {name: "Bond"});

上邊的代碼實(shí)現(xiàn)了按照對象的屬性排序的功能褥符。當(dāng)我們在一個(gè)函數(shù)的內(nèi)部定義了另一個(gè)函數(shù)龙誊,那么在該函數(shù)執(zhí)行時(shí),就會把該函數(shù)的活動對象添加到它內(nèi)部的函數(shù)的作用域鏈之中喷楣。這也就是為什么compare()函數(shù)為什么能訪問createCompareFunction內(nèi)部參數(shù)的原因趟大。

更為重要的是,createCompareFunction()函數(shù)在執(zhí)行完畢后铣焊,器活動對象也不會被銷毀逊朽,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動對象。換句話說曲伊,當(dāng)createCompareFunction()函數(shù)返回后叽讳,其執(zhí)行環(huán)境的作用域鏈會被銷毀,但它的活動對象人讓親會留在內(nèi)存中坟募,直到匿名函數(shù)被銷毀之后岛蚤,createCompareFunction()函數(shù)的活動對象才會被銷毀。

const compare = createCompareFunction("name");
const result = compare({name: "James"}, {name: "Bond"});
compare = null;

這其中關(guān)于閉包最終要的問題就是懈糯,他內(nèi)部的作用域鏈中會有一個(gè)外部函數(shù)的活動對象的引用涤妒。

我們看看上邊代碼執(zhí)行過程中發(fā)生了什么:

由于閉包會攜帶包含它的函數(shù)的作用域,因此會比其他函數(shù)占用更多的內(nèi)存赚哗。過度使用閉包可能會導(dǎo)致內(nèi)存占用過多她紫,建議大家只在絕對必要時(shí)再考慮使用閉包。

但是閉包在使用不當(dāng)?shù)那闆r下會產(chǎn)生一定的副作用屿储,上文中贿讹,我們反復(fù)提到,閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值够掠,因?yàn)殚]包保存的是整個(gè)變量對象民褂,而是不是某個(gè)特殊的變量。

function createFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }
    return result;
}

const funcs = createFunctions();
console.log(funcs[2]());

上邊的代碼中,看似createFunctions()函數(shù)應(yīng)該返回一個(gè)函數(shù)數(shù)組助赞,數(shù)組中的每個(gè)函數(shù)都應(yīng)該返回自己的索引值买羞,但實(shí)際上,每個(gè)函數(shù)都返回10雹食。createFunctions()函數(shù)返回的閉包中保存的是createFunctions()函數(shù)的活動對象畜普,這個(gè)活動對象中的其中一個(gè)屬性就是i。createFunctions()函數(shù)執(zhí)行完畢后群叶,i變成了10吃挑,因此當(dāng)我們調(diào)用閉包函數(shù)的時(shí)候,他其實(shí)是去訪問了活動對象中的i街立〔俺模基于這個(gè)原理,我們可以使用這種方式:

function createFunctions() {
    var result = new Array();
    for (var i = 0; i < 10; i++) {
        result[i] = function (num) {
            return function () {
                return num;
            };
        }(i);
    }
    return result;
}

const funcs = createFunctions();
console.log(funcs[2]());

殺精編的例子中赎离,用了兩層閉包逛犹,最內(nèi)層的閉包訪問num,外層的閉包訪問i并且立即執(zhí)行梁剔。

在閉包中使用this對象也可能會導(dǎo)致一些問題虽画。我們知道,this對象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中荣病,this等于window码撰,而當(dāng)函數(shù)被作為某個(gè)對象的方法調(diào)用時(shí),this等于那個(gè)對象个盆。

匿名函數(shù)的執(zhí)行環(huán)境具有全局性脖岛,在沒有指定調(diào)用對象的前提下,this對象通常指向window》

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        return function () {
            return this.name;
        }
    }
};

console.log(object.getNameFunction()());

上邊的代碼在調(diào)用了object.getNameFunction()返回了一個(gè)函數(shù)颊亮,然后在調(diào)用這個(gè)返回的函數(shù)柴梆,就返回了“The window”。

這里唯一的問題是终惑,為什么匿名函數(shù)沒有取得其包含作用域(或外部作用域)的this對象呢轩性?

前面我們曾經(jīng)提到過,每個(gè)函數(shù)在被調(diào)用時(shí)狠鸳,其活動對象都會自動獲取兩個(gè)特殊變量:this和arguments。內(nèi)部函數(shù)在搜索這兩個(gè)變量時(shí)悯嗓,只會搜索到其活動對象為止件舵,因此永遠(yuǎn)不可能直接訪問外部函數(shù)中的這兩個(gè)變量。

這說明訪問內(nèi)部函數(shù)的參數(shù)時(shí)脯厨,得到的就是內(nèi)部函數(shù)的參數(shù)铅祸,而不是其他值,否則就亂套了。

把外部作用域中的this對象保存在一個(gè)閉包能夠訪問到的變量中临梗,就可以讓閉包訪問該對象了:

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        const that = this;
        return function () {
            return that.name;
        }
    }
};

console.log(object.getNameFunction()());

記住涡扼,this和arguments這兩個(gè)比較特殊,只能訪問自身的活動對象盟庞。

在幾種特殊的情況下吃沪,this的值可能會意外的改變:

const name = "The window";
const object = {
    name: "My object",
    getNameFunction: function () {
        return this.name;
    }
};

console.log(object.getNameFunction()); // => "My object"
console.log((object.getNameFunction)()); // => "My object"
console.log((object.getNameFunction = object.getNameFunction)()); // => "The window"

最后一行代碼比較有意思,賦值表達(dá)式的結(jié)果就是函數(shù)什猖,然后調(diào)用函數(shù)之后就打印出了"The window"票彪。

本篇大部分內(nèi)容來源于<<JavaScript高級程序設(shè)計(jì)>>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市不狮,隨后出現(xiàn)的幾起案子降铸,更是在濱河造成了極大的恐慌,老刑警劉巖摇零,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件推掸,死亡現(xiàn)場離奇詭異,居然都是意外死亡驻仅,警方通過查閱死者的電腦和手機(jī)谅畅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來雾家,“玉大人铃彰,你說我怎么就攤上這事⌒具郑” “怎么了牙捉?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長敬飒。 經(jīng)常有香客問我邪铲,道長,這世上最難降的妖魔是什么无拗? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任带到,我火速辦了婚禮,結(jié)果婚禮上英染,老公的妹妹穿的比我還像新娘揽惹。我一直安慰自己,他們只是感情好四康,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布搪搏。 她就那樣靜靜地躺著,像睡著了一般闪金。 火紅的嫁衣襯著肌膚如雪疯溺。 梳的紋絲不亂的頭發(fā)上论颅,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音囱嫩,去河邊找鬼恃疯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛墨闲,可吹牛的內(nèi)容都是我干的今妄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼损俭,長吁一口氣:“原來是場噩夢啊……” “哼蛙奖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杆兵,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤雁仲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后琐脏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體攒砖,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年日裙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吹艇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昂拂,死狀恐怖受神,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情格侯,我是刑警寧澤鼻听,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站联四,受9級特大地震影響撑碴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朝墩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一醉拓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧收苏,春花似錦亿卤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杜跷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背葛闷。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工憋槐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人淑趾。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓阳仔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親扣泊。 傳聞我的和親對象是個(gè)殘疾皇子近范,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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