徹底理解js閉包

本文主要參考MDN手冊Learning Advanced JavaScript

在文章開頭玩徊,我先放出MDN給出的定義:

閉包是指那些能夠訪問獨立(自由)變量的函數(shù) (變量在本地使用恩袱,但定義在一個封閉的作用域中)。換句話說潭辈,這些函數(shù)可以“記憶”它被創(chuàng)建時候的環(huán)境澈吨。

現(xiàn)在不需要看懂它,我會在第一個例子中解釋清楚它的意思修赞。讓我們開始吧桑阶!

2018.3.20更新:現(xiàn)在MDN上的定義已經(jīng)改為:"A closure is the combination of a function and the lexical environment within which that function was declared."


要理解函數(shù)閉包勾邦,就要先知道這兩條特性:

  1. 函數(shù)外部的代碼無法訪問函數(shù)體內(nèi)部的變量割择,而函數(shù)體內(nèi)部的代碼可以訪問函數(shù)外部的變量锨推。
  2. 即使函數(shù)已經(jīng)執(zhí)行完畢,在執(zhí)行期間創(chuàng)建的變量也不會被銷毀椎椰,因此每運行一次函數(shù)就會在內(nèi)存中留下一組變量沾鳄。(js當然會有垃圾回收機制,不過如果它發(fā)現(xiàn)你正在使用閉包译荞,則不會清理可能會用到的變量)

使用閉包能產(chǎn)生類似于對象的一組變量集合,看個例子:

function outter() {
  var private= "I am private";
  function show() {
    console.log(private);
  }
  return show;
}

var ref = outter();
// console.log(private); // 嘗試直接訪問private會報錯:private is not defined
ref(); // 打印I am private

我們調(diào)用了一次outter函數(shù)圈膏,產(chǎn)生了一組變量:private和show篙骡。要不是我們在outter最后一句返回了show,這兩個變量就永遠沒辦法被訪問到了(因為函數(shù)外部的代碼無法訪問函數(shù)體內(nèi)部的變量)尿褪。但是我們現(xiàn)在返回了show得湘,并且ref是show的引用,這樣我們就可以在函數(shù)體外部調(diào)用show了摆马,而show又可以訪問到private鸿吆。
這不就像是一個C++或JAVA中的對象嗎?private就是它的私有成員,嘗試從外部直接訪問它就會收到報錯黎泣;show就是它的公有成員,所以我們可以在外部訪問到它褐着,并且它可以訪問私有成員name托呕。使用閉包能產(chǎn)生類似于對象的一組變量集合。

現(xiàn)在我們對照著這個例子來理解閉包的定義:

閉包是指那些能夠訪問獨立(自由)變量的函數(shù) (變量在本地使用馅扣,但定義在一個封閉的作用域中)着降。換句話說,這些函數(shù)可以“記憶”它被創(chuàng)建時候的環(huán)境蓄喇。

我初學的時候犯了一個錯誤交掏,就是認為outter是閉包函數(shù)(因為我以為將整個閉包結(jié)構(gòu)“包”起來的函數(shù)就是閉包函數(shù)),但其實根據(jù)定義钱骂,被返回的show才是閉包函數(shù)熊尉,也就是那個可以在外部訪問“私有成員”的函數(shù)。

  • 定義中的“獨立(自由)變量”其實就是我們剛才說的私有成員张吉,它們是獨立(自由)的催植,是因為定義它的函數(shù)已經(jīng)死了(執(zhí)行完畢)创南!
  • “變量在本地使用,但定義在一個封閉的作用域中”的意思是稿辙,自由變量可以在閉包函數(shù)中使用,但是自由變量并不是在閉包中定義的赋咽。
  • “閉包函數(shù)可以“記憶”它被創(chuàng)建時候的環(huán)境”的意思是脓匿,outter執(zhí)行的過程產(chǎn)生了一組變量,這些變量就是show被聲明時候的環(huán)境陪毡。show可以記住這個環(huán)境(變量private)毡琉,即使show離開了outter(被return到外部),它依然記得如何訪問這個環(huán)境里的變量精拟。

讓我們再看一個例子

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

這一次虱歪,makeAdder創(chuàng)建了一個閉包結(jié)構(gòu)笋鄙,傳入的參數(shù)x就是執(zhí)行期間創(chuàng)建的臨時變量,它就相當于是私有成員(自由變量)萧落。而公有成員是一個匿名函數(shù)找岖,這個函數(shù)接受一個參數(shù)y,并將這個參數(shù)與閉包的私有成員x相加兴革,返回結(jié)果蜜唾。

這個例子有意思的地方在于:makeAdder調(diào)用了兩次!每運行一次makeAdder就會在內(nèi)存中產(chǎn)生一組變量(也就是一個“環(huán)境”)擎勘,每一個“環(huán)境”雖然結(jié)構(gòu)相同颖榜,都有私有成員x和公有成員函數(shù)煤裙,但是這兩個“環(huán)境”是互不干涉的积暖。在這個例子中怪与,第一個環(huán)境中x=5缅疟,第二個環(huán)境中x=10。

利用閉包的特性耘斩,可以實現(xiàn)模塊模式桅咆。用一個閉包函數(shù)包裹模塊的代碼岩饼,將不需要暴露的變量隱藏起來(好處是不會污染全局變量空間),將別人要調(diào)用的方法return出去籍茧,就可以實現(xiàn)模塊化了寞冯。實際上Node.js就是這么做的,看我的另一篇文章俭茧。


經(jīng)典面試題

讓我們再看一個常見的錯誤:在循環(huán)中創(chuàng)建閉包

...
<ol>
    <li>第一項</li>
    <li>第二項</li>
    <li>第三項</li>
    <li>第四項</li>
</ol>
...
window.onload = function() {  //  函數(shù)1
    var lis = document.getElementsByTagName('li');
    for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = function() {  //  函數(shù)2
            alert(i);
        }
    }
}

不管我們點擊哪一個li元素漓帚,都會顯示3胰默,而不是分別顯示0到3的數(shù)字因块。這是為什么呢?
這是因為,函數(shù)2被聲明了4次沒錯渣淤,但它們是在同一個環(huán)境中被聲明的(都是在執(zhí)行函數(shù)1的環(huán)境)!因此這四個函數(shù)2“記住”的是同一個i扁耐!當我們點擊li元素時产阱,循環(huán)早已完成,i停在了3王暗。因此庄敛,我們不管點擊哪一個li元素,總是會顯示3绷雏。
既然我們已經(jīng)知道了錯誤的原因怖亭,那么修改的思路也很明確了:四個onclick函數(shù)必須有自己“聲明環(huán)境”依许!既然要產(chǎn)生4個“環(huán)境”,那么就說明必須有一個函數(shù)在for循環(huán)內(nèi)運行膘婶,總共運行4次蛀醉,每次的環(huán)境中都有一個變量private_i,分別等于0脊岳、1垛玻、2帚桩、3。

修改后:

...
<ol>
    <li>第一項</li>
    <li>第二項</li>
    <li>第三項</li>
    <li>第四項</li>
</ol>
...
window.onload = function() {  //  函數(shù)1
    var lis = document.getElementsByTagName('li');
    for (var i = 0; i < lis.length; i++) {
        lis[i].onclick = (function(private_i) {  //  函數(shù)2
            return function() {  //  函數(shù)3
                alert(private_i);
            }
        })(i);  //  這里將i作為參數(shù)莫瞬,調(diào)用函數(shù)2
    }
}

注意賦值給onclick的并不是函數(shù)2,而是函數(shù)2的執(zhí)行結(jié)果喂江,也就是函數(shù)3旁振。函數(shù)3內(nèi)的private_i是函數(shù)2調(diào)用時所產(chǎn)生的拐袜,而函數(shù)2總共調(diào)用了4次,為4個函數(shù)3都分別留下了一個“環(huán)境”private_i,四個private_i分別是0沮尿、1畜疾、2、3姥敛。因此瞎暑,點擊4個li元素就會顯示出4個不同的數(shù)字了。


不要隨便在函數(shù)中創(chuàng)建函數(shù)

除非明確你知道你自己需要使用閉包墨榄,否則勿她,不要在函數(shù)中創(chuàng)建另一個函數(shù),這樣會造成速度和性能的浪費之剧。
看一個MDN上的例子:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };
  this.getMessage = function() {
    return this.message;
  };
}
var obj1 = new MyObject("name1", "message1");
var obj2 = new MyObject("name2", "message2");

每次執(zhí)行MyObject背稼,都會在內(nèi)存中創(chuàng)建出兩個函數(shù)辩恼,每次創(chuàng)建的getName函數(shù)都是一樣的,getMessage函數(shù)也是一樣疆前,造成了不必要的浪費竹椒。

實際上,如果我們要讓對象都有一樣的方法书释,只需要在它們的prototype上定義這個方法就行了:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};
var obj1 = new MyObject("name1", "message1");
var obj2 = new MyObject("name2", "message2");

這樣赊窥,函數(shù)只創(chuàng)建了一次锨能,而obj1和obj2都能繼承到getName和getMessage方法。
有關(guān)原型繼承的細節(jié)熄阻,請閱讀我的另一篇文章《徹底理解js的原型鏈》


閉包函數(shù)究竟是怎么“記住”創(chuàng)建時期的環(huán)境的秃殉?如果你對其原理感到好奇的話钾军,可以看看我的這兩篇文章:
js的執(zhí)行上下文乒省,以及其中的變量對象
徹底理解js的作用域鏈

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末袖扛,一起剝皮案震驚了整個濱河市蛆封,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盏筐,老刑警劉巖砸讳,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異漾抬,居然都是意外死亡,警方通過查閱死者的電腦和手機挽荠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門圈匆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捏雌,“玉大人,你說我怎么就攤上這事来累【阶啵” “怎么了葫录?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵米同,是天一觀的道長面粮。 經(jīng)常有香客問我,道長稍走,這世上最難降的妖魔是什么柴底? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任柄驻,我火速辦了婚禮,結(jié)果婚禮上抑钟,老公的妹妹穿的比我還像新娘。我一直安慰自己幻件,他們只是感情好,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布心俗。 她就那樣靜靜地躺著傲武,像睡著了一般。 火紅的嫁衣襯著肌膚如雪城榛。 梳的紋絲不亂的頭發(fā)上揪利,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音狠持,去河邊找鬼疟位。 笑死,一個胖子當著我的面吹牛喘垂,可吹牛的內(nèi)容都是我干的甜刻。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼得院,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了章贞?” 一聲冷哼從身側(cè)響起祥绞,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸭限,沒想到半個月后蜕径,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡败京,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年兜喻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赡麦。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡朴皆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泛粹,到底是詐尸還是另有隱情车荔,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布戚扳,位于F島的核電站忧便,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜珠增,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一超歌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒂教,春花似錦巍举、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至梦皮,卻和暖如春炭分,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剑肯。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工捧毛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人让网。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓呀忧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親溃睹。 傳聞我的和親對象是個殘疾皇子而账,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351

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

  • 閉包并不是JS所獨有的,在計算機科學中其是一個普遍的概念因篇,在Python中也有閉包的概念福扬,但閉包在Python應用...
    bruce_zhou閱讀 1,760評論 2 9
  • 閉包(closure)是Javascript語言的一個難點,也是它的特色惜犀,很多高級應用都要依靠閉包實現(xiàn)。 一狠裹、變量...
    zock閱讀 1,075評論 2 6
  • 最近學這塊知識學得有些吃力虽界。還有很多遺漏的地方,只能以后多看些書來彌補了涛菠。 第7章 函數(shù)表達式 函數(shù)定義的兩種方式...
    丨ouo丨閱讀 365評論 0 1
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young閱讀 3,788評論 1 10
  • 先來講一則故事: 一對父子坐在公園的長椅上俗冻,父親指著樹上的麻雀問兒子:那是什么呀礁叔? 兒子答:那是麻雀。 過了幾分鐘...
    娟子文閱讀 510評論 0 6