Javascript的閉包

對js的廣大初學(xué)者來說芹血,閉包絕對是個難點。而且經(jīng)常出現(xiàn)今天感覺懂了楞慈,明天就又不懂了的情況祟牲。本文就嘗試從我自己的學(xué)習(xí)體會出發(fā),嘗試把這個概念講清楚抖部。
簡單來說说贝,閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)
下面這個函數(shù)是一個根據(jù)初始值自加的函數(shù)慎颗。

function count(init) {

    return function() {
        init++;
        return init;
    }
}

var f1 = count(1);
console.log(f1());  //2
console.log(f1());  //3

var f2 = count(11);
console.log(f2());  //12
console.log(f2());  //13

上面就是一個閉包的例子乡恕。count函數(shù)在執(zhí)行完之后返回了內(nèi)部匿名函數(shù),并賦值給f1和f2俯萎,f1和f2依然可以訪問count函數(shù)中init變量傲宜,f1和f2就是兩個閉包。
要搞清楚其中的細(xì)節(jié)夫啊,我們就必須理解f1和f2在第一次調(diào)用的時候到底發(fā)生了什么函卒。我們首先來看兩個基本觀念:執(zhí)行環(huán)境及作用域。

執(zhí)行環(huán)境及作用域

執(zhí)行環(huán)境

執(zhí)行環(huán)境(execution context撇眯,有時直接簡稱為“環(huán)境”)是ECMAScirpt中最為重要的一個概念报嵌,用來描述js代碼執(zhí)行的抽象概念。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù)熊榛,決定了它們各自的行為锚国。換句話說,所有的js都是在某個執(zhí)行環(huán)境中運行的玄坦,我們可以把執(zhí)行環(huán)境想成一個執(zhí)行js代碼的盒子血筑。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的變量對象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中煎楣。
全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境豺总,根據(jù)ECMAScript實現(xiàn)所在的宿主環(huán)境的不同,表示執(zhí)行環(huán)境的對象也不一樣择懂。在Web瀏覽器中喻喳,全局執(zhí)行環(huán)境被認(rèn)為是window對象,因此所有全局變量和函數(shù)都是作為window對象的屬性和方法創(chuàng)建的休蟹。某個執(zhí)行環(huán)境的所有代碼執(zhí)行完畢后沸枯,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀赂弓。
每個函數(shù)都有自己的執(zhí)行環(huán)境绑榴。當(dāng)執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入到環(huán)境棧中盈魁。而在函數(shù)執(zhí)行之后翔怎,棧將其環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境杨耙。

作用域鏈

當(dāng)js代碼在一個環(huán)境中執(zhí)行時赤套,會創(chuàng)建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途珊膜,是保證對執(zhí)行環(huán)境有權(quán)訪問的所有變量和函數(shù)的有序訪問容握。作用域鏈的前端, 始終是當(dāng)前執(zhí)行代碼所在環(huán)境的變量對象. 如果這個環(huán)境是一個函數(shù), 則將其活動對象(activation object)作為變量對象. 活動對象在最開始時只包含一個變量, 即arguments對象(這個對象在全局環(huán)境中是不存在的). 作用域鏈中的下一個變量對象來自包含(外部)環(huán)境, 而再下一個變量對象則來自下一個包含環(huán)境. 這樣一直延續(xù)到全局執(zhí)行環(huán)境.
標(biāo)識符解析是沿著作用域鏈一級一級地搜索標(biāo)識符的過程. 搜索過程始終從作用域鏈的前端開始, 然后逐級地向后回溯, 直至找到標(biāo)識符為止(如果找不到標(biāo)識符, 通常導(dǎo)致錯誤發(fā)生)

閉包

我們再來看看我們的demo

function count(init) {

    return function() {
        init++;
        return init;
    }
}

var f1 = count(1);
console.log(f1());  //2
console.log(f1());  //3

f1之所以還能訪問 變量 init, 是因為f1函數(shù)的作用域鏈包含 count函數(shù)的作用域.
下面是最關(guān)鍵的部分:

  1. 在創(chuàng)建count()函數(shù)時,會創(chuàng)建一個預(yù)先包含全局變量對象的作用域鏈车柠,這個作用域鏈被保存在內(nèi)部的[[Scope]]屬性中剔氏。
  2. 當(dāng)調(diào)用count()函數(shù)時,會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境竹祷,然后通過復(fù)制函數(shù)的[[Scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈. 此后, count()函數(shù)的活動對象被創(chuàng)建, 并被推入到執(zhí)行環(huán)境作用域鏈的前端.
  3. 在count()函數(shù)內(nèi)部的匿名函數(shù)會將count()函數(shù)的執(zhí)行環(huán)境的作用域鏈初始化成自己的作用域鏈中. 這樣匿名函數(shù)就可以訪問count()函數(shù)中的所有變量了.
  4. 當(dāng)count()函數(shù)中的匿名函數(shù)最終返回并賦值給f1, f1的作用域鏈就包含全局變量對象和count()函數(shù)的活動對象, 所以count()函數(shù)的活動對象不會被銷毀. 換句話說, count()函數(shù)執(zhí)行完畢后, count()函數(shù)的執(zhí)行環(huán)境被銷毀, 但是count()函數(shù)的活動對象直到f1被銷毀后, 才會被銷毀.

到這里我們就明白了, 只要你在一個函數(shù)內(nèi)部定義了另一個函數(shù), 閉包就產(chǎn)生了.

this對象

在閉包中使用this對象會遇到一些問題. 我們知道this對象指向了當(dāng)前代碼的執(zhí)行環(huán)境. 也就是說, 在全局環(huán)境中this等于window(瀏覽器環(huán)境), 當(dāng)被當(dāng)做某個對象的方法調(diào)用時, this指向的就是那個方法.

當(dāng)然, 也可以通過apply()和call()改變函數(shù)的執(zhí)行環(huán)境

我們看一下下面的例子:

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function () {
        return function () {
            return this.name;
        };
    }
};

console.log(object.getNameFunc()());

這時候return回來的是"The Window", 而不是"My Object"
我們分解一下來看:

  1. object.getNameFunc()執(zhí)行時, getNameFunc()是作為object的方法執(zhí)行的, this指向object, 然后返回一個匿名函數(shù).
  2. 這個匿名函數(shù)在調(diào)用的時候, 實際上是在全局環(huán)境中執(zhí)行的, 所以this指向全局環(huán)境, 返回this.name就是"The Window"

如果我們想返回"My Object"該咋辦? 那我們就得想著怎么把第一步中的this傳到第二步的匿名函數(shù)中.

    getNameFunc : function () {
        var that = this;
        return function () {
            return that.name;
        };
    }

在定義匿名函數(shù)前, 我們把this保存在that變量中, 這樣閉包也可以訪問that變量.

模仿塊級作用域

我們知道Javascript中沒有塊級作用域, 也就是定義塊中變量, 它的作用域是當(dāng)前函數(shù), 和塊沒有關(guān)系. 我們可以利用函數(shù)的作用域來模仿塊級作用域.

!function() {
    var i = 10;
    console.log(i); //10
}();

console.log(i+1);   //i is not defined

我們創(chuàng)建了一個函數(shù)并立即調(diào)用它, 這樣其中的代碼執(zhí)行了, 而且因為函數(shù)執(zhí)行完畢, 它的執(zhí)行環(huán)境和其中的變量對象都會被銷毀, 所以下面的代碼提示i is not defined

封裝

面向?qū)ο蟮娜蠡痪褪欠庋b. 封裝簡單來說就是只公開代碼單元的對外接口, 而隱藏內(nèi)部的具體實現(xiàn).
Javascript是面向?qū)ο蟮恼Z言, 那它如何實現(xiàn)封裝呢? 我們知道Javascript中沒有私有成員的概念, 所有對象的屬性都是公開的. 但是呢, Javascript有私有變量的概念, 函數(shù)內(nèi)部的變量外部是無法訪問的. 這里, 我們就可以利用閉包來完成封裝.

function Account() {
    var balance = 0;
    function save(money){
        balance += money;
        query();
    }

    function draw(money){
        if(money > balance){
            balance = 0;
        }
        else{
            balance -= money;
        }
        query();
    }
    
    function query(){
        console.log("Your balance is " + balance);
    }

    return {
        Save : function(money){
            save(money);
        },
        Draw : function(money){
            draw(money);
        }
    }
}

var acount = new Account();

acount.Save(10);
acount.Draw(5);

acount.save(10);    //save is not a function
console.log(acount.balance);    //undefined

例子是個銀行賬戶對象, 對外公開了存錢和取錢兩種操作. 這里用工廠模式來創(chuàng)建對象, 用構(gòu)造函數(shù)也是同樣的道理. 我們把有權(quán)訪問私有變量和方法的公有方法成為特權(quán)方法(Save和Draw方法)

呼呼, 好像我想說的都說完了, 下面開始一分鐘滿分作文時間, 來回顧一下我們都學(xué)到了什么:

  • 當(dāng)在函數(shù)內(nèi)部定義了其他函數(shù)時, 就創(chuàng)建了閉包. 閉包有權(quán)訪問函數(shù)內(nèi)部的所有變量.
    -閉包的作用域鏈, 包含著自己的作用域, 包含函數(shù)的作用域和全局的作用域
    -通常, 函數(shù)的作用域和變量會在函數(shù)調(diào)用結(jié)束后銷毀.
    -但是, 當(dāng)函數(shù)返回了閉包時, 函數(shù)的作用域會一直保存直到閉包不存在為止
  • 創(chuàng)建并立即調(diào)用函數(shù)可以模仿塊級作用域
  • 閉包可以實現(xiàn)封裝
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谈跛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子塑陵,更是在濱河造成了極大的恐慌感憾,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件令花,死亡現(xiàn)場離奇詭異阻桅,居然都是意外死亡,警方通過查閱死者的電腦和手機兼都,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門鳍刷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人俯抖,你說我怎么就攤上這事输瓜。” “怎么了芬萍?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵尤揣,是天一觀的道長。 經(jīng)常有香客問我柬祠,道長北戏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任漫蛔,我火速辦了婚禮嗜愈,結(jié)果婚禮上旧蛾,老公的妹妹穿的比我還像新娘。我一直安慰自己蠕嫁,他們只是感情好锨天,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著剃毒,像睡著了一般病袄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赘阀,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天益缠,我揣著相機與錄音,去河邊找鬼基公。 笑死幅慌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轰豆。 我是一名探鬼主播欠痴,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼秒咨!你這毒婦竟也來了喇辽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤雨席,失蹤者是張志新(化名)和其女友劉穎菩咨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體陡厘,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡抽米,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了糙置。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片云茸。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谤饭,靈堂內(nèi)的尸體忽然破棺而出标捺,到底是詐尸還是另有隱情,我是刑警寧澤揉抵,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布亡容,位于F島的核電站,受9級特大地震影響冤今,放射性物質(zhì)發(fā)生泄漏闺兢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一戏罢、第九天 我趴在偏房一處隱蔽的房頂上張望屋谭。 院中可真熱鬧脚囊,春花似錦、人聲如沸桐磁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽所意。三九已至,卻和暖如春催首,著一層夾襖步出監(jiān)牢的瞬間扶踊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工郎任, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秧耗,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓舶治,卻偏偏與公主長得像分井,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子霉猛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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