JavaScript高級(jí)程序設(shè)計(jì)——閉包

js-closure

前言

有很多人搞不清匿名函數(shù)閉包這兩個(gè)概念,經(jīng)常混用锋拖。閉包是指有權(quán)訪問(wèn)另一個(gè)函數(shù)作用域中的變量的函數(shù)。匿名函數(shù)就是沒(méi)有實(shí)際名字的函數(shù)祸轮。

閉包

概念

閉包兽埃,其實(shí)是一種語(yǔ)言特性,它是指的是程序設(shè)計(jì)語(yǔ)言中适袜,允許將函數(shù)看作對(duì)象柄错,然后能像在對(duì)象中的操作搬在函數(shù)中定義實(shí)例(局部)變量,而這些變量能在函數(shù)中保存到函數(shù)的實(shí)例對(duì)象銷毀為止苦酱,其它代碼塊能通過(guò)某種方式獲取這些實(shí)例(局部)變量的值并進(jìn)行應(yīng)用擴(kuò)展售貌。

條件

閉包是允許函數(shù)訪問(wèn)局部作用域之外的數(shù)據(jù)。即使外部函數(shù)已經(jīng)退出疫萤,外部函數(shù)的變量仍可以被內(nèi)部函數(shù)訪問(wèn)到颂跨。

因此閉包的實(shí)現(xiàn)需要三個(gè)條件:

內(nèi)部函數(shù)實(shí)用了外部函數(shù)的變量
外部函數(shù)已經(jīng)退出
內(nèi)部函數(shù)可以訪問(wèn)

function a() {
            var x = 0;
            return function(y) {
                x = x + y;
                // return x;
                console.log(x);

            }

        }
        var b = a();
        b(1); //1
        b(1); //2

上述代碼在執(zhí)行的時(shí)候,b得到的是閉包對(duì)象的引用扯饶,雖然a執(zhí)行完畢后恒削,但是a的活動(dòng)對(duì)象由于閉包的存在并沒(méi)有被銷毀池颈,在執(zhí)行b(1)的時(shí)候,仍然訪問(wèn)到了x變量钓丰,并將其加1躯砰,若再執(zhí)行b(1),則x是2携丁,因?yàn)殚]包的引用b并沒(méi)有消除琢歇。(后面會(huì)解釋,閉包返回了函數(shù)则北,函數(shù)可以創(chuàng)建獨(dú)立的作用域)

閉包矿微,其實(shí)就是指程序語(yǔ)言中能讓代碼調(diào)用已運(yùn)行的函數(shù)中所定義的局部變量。

但是你只需要知道應(yīng)用的兩種情況即可——函數(shù)作為返回值尚揣,函數(shù)作為參數(shù)傳遞涌矢。

function fn() {
            var max = 10;
            return function bar(x) {
                if (x > max) {
                    console.log(x);
                }
            };
        }
        var f1 = fn();
        f1(15);

如上代碼,bar函數(shù)作為返回值快骗,賦值給f1變量娜庇。執(zhí)行f1(15)時(shí),用到了fn作用域下的max變量的值方篮。至于如何跨作用域取值名秀,可以參考上一篇文章。

var max = 10,
            fn = function(x) {
                if (x > max) {
                    console.log(x);  //15
                }
            };
        (function(f) {
            var max = 100;
            f(15);
        })(fn); 

如上代碼中藕溅,fn函數(shù)作為一個(gè)參數(shù)被傳遞進(jìn)入另一個(gè)函數(shù)匕得,賦值給f參數(shù)。執(zhí)行f(15)時(shí)巾表,max變量的取值是10汁掠,而不是100。

上一篇講到自由變量跨作用域取值時(shí)集币,曾經(jīng)強(qiáng)調(diào)過(guò):要去創(chuàng)建這個(gè)函數(shù)的作用域取值考阱,而不是“父作用域”。理解了這一點(diǎn)鞠苟,以上兩端代碼中乞榨,自由變量如何取值應(yīng)該比較簡(jiǎn)單.

另外,講到閉包当娱,除了結(jié)合著作用域之外吃既,還需要結(jié)合著執(zhí)行上下文棧來(lái)說(shuō)一下。

在前面講執(zhí)行上下文棧時(shí)趾访,我們提到當(dāng)一個(gè)函數(shù)被調(diào)用完成之后态秧,其執(zhí)行上下文環(huán)境將被銷毀,其中的變量也會(huì)被同時(shí)銷毀扼鞋。

有些情況下申鱼,函數(shù)調(diào)用完成之后,其執(zhí)行上下文環(huán)境不會(huì)接著被銷毀云头。這就是需要理解閉包的核心內(nèi)容捐友。

可以拿本文的之前代碼(只做注釋修改)來(lái)分析一下。

1//全局作用域
2        function fn() {
3            var max = 10;
4            // fn作用域
5            return function bar(x) {
6                if (x > max) {
7                    console.log(x);
8                }
9            }; //bar作用域
10        }
11       var f1 = fn();
12        f1(15);

全局作用域?yàn)椋捍a1-12行溃槐;fn作用域?yàn)椋捍a2-10行匣砖;bar作用域?yàn)椋捍a5-9行。


舉例

第一步昏滴,代碼執(zhí)行前生成全局上下文環(huán)境猴鲫,并在執(zhí)行時(shí)對(duì)其中的變量進(jìn)行賦值。此時(shí)全局上下文環(huán)境是活動(dòng)狀態(tài)谣殊。

js-closure

第二步拂共,執(zhí)行第17行代碼時(shí),調(diào)用fn()姻几,產(chǎn)生fn()執(zhí)行上下文環(huán)境宜狐,壓棧,并設(shè)置為活動(dòng)狀態(tài)蛇捌。


js-closure

第三步抚恒,執(zhí)行完第17行,fn()調(diào)用完成络拌。按理說(shuō)應(yīng)該銷毀掉fn()的執(zhí)行上下文環(huán)境俭驮,但是這里不能這么做。注意春贸,重點(diǎn)來(lái)了:

因?yàn)閳?zhí)行fn()時(shí)混萝,返回的是一個(gè)函數(shù)。函數(shù)的特別之處在于可以創(chuàng)建一個(gè)獨(dú)立的作用域祥诽。而正巧合的是譬圣,返回的這個(gè)函數(shù)體中,還有一個(gè)自由變量max要引用fn作用域下的fn()上下文環(huán)境中的max雄坪。因此厘熟,這個(gè)max不能被銷毀,銷毀了之后bar函數(shù)中的max就找不到值了维哈。

因此绳姨,這里的fn()上下文環(huán)境不能被銷毀,還依然存在與執(zhí)行上下文棧中阔挠。

——即飘庄,執(zhí)行到第18行時(shí),全局上下文環(huán)境將變?yōu)榛顒?dòng)狀態(tài)购撼,但是fn()上下文環(huán)境依然會(huì)在執(zhí)行上下文棧中跪削。另外谴仙,執(zhí)行完第18行,全局上下文環(huán)境中的max被賦值為100碾盐。如下圖:


js-closure

第四步晃跺,執(zhí)行到第20行,執(zhí)行f1(15)毫玖,即執(zhí)行bar(15)掀虎,創(chuàng)建bar(15)上下文環(huán)境,并將其設(shè)置為活動(dòng)狀態(tài)付枫。


js-closure

執(zhí)行bar(15)時(shí)烹玉,max是自由變量,需要向創(chuàng)建bar函數(shù)的作用域中查找阐滩,找到了max的值為10二打。這個(gè)過(guò)程在作用域鏈一節(jié)已經(jīng)講過(guò)。

這里的重點(diǎn)就在于叶眉,創(chuàng)建bar函數(shù)是在執(zhí)行fn()時(shí)創(chuàng)建的址儒。fn()早就執(zhí)行結(jié)束了,但是fn()執(zhí)行上下文環(huán)境還存在與棧中衅疙,因此bar(15)時(shí)莲趣,max可以查找到。如果fn()上下文環(huán)境銷毀了饱溢,那么max就找不到了喧伞。

總結(jié):使用閉包會(huì)增加內(nèi)容開(kāi)銷

第五步,執(zhí)行完20行就是上下文環(huán)境的銷毀過(guò)程绩郎,這里就不再贅述了潘鲫。

閉包與變量

概念

閉包只能取得包含函數(shù)中任何變量的最后一個(gè)值,閉包所保存的是整個(gè)變量對(duì)象,而不是某個(gè)特殊變量肋杖。

例子

function createFunctions() {
            var result = new Array();

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

            return result;
        }

        var funcs = createFunctions();

        //每個(gè)函數(shù)都輸出10
        for (var i = 0; i < funcs.length; i++) {
            document.write(funcs[i]() + "<br />");
        }

總結(jié):每個(gè)函數(shù)的作用域鏈中都保存著createFunctions()函數(shù)的活動(dòng)對(duì)象溉仑,所以它們引用的都是同一個(gè)變量i。當(dāng)createFunctions()函數(shù)返回后状植,變量i的值為10浊竟。

我們可以通過(guò)創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為符合預(yù)期。

function createFunctions() {
            var result = new Array();

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

            return result;
        }

        var funcs = createFunctions();

        //循環(huán)輸出0-10
        for (var i = 0; i < funcs.length; i++) {
            document.write(funcs[i]() + "<br />");
        }

總結(jié):沒(méi)有直接把閉包賦值給數(shù)組津畸,而是定義了一個(gè)匿名函數(shù)振定,并通過(guò)立即執(zhí)行該匿名函數(shù)的結(jié)果賦值給數(shù)組,并帶了for循環(huán)的參數(shù)i進(jìn)去肉拓,讓x能找到傳入的參數(shù)值為0-10后频,這就解釋了函數(shù)參數(shù)是按值傳遞的,所以會(huì)將變量i的當(dāng)前值復(fù)制給參數(shù)x暖途。而這個(gè)匿名函數(shù)內(nèi)部又創(chuàng)建并返回了一個(gè)訪問(wèn)x的閉包卑惜。這樣以來(lái)result數(shù)組中的每個(gè)函數(shù)都有自己x變量的一個(gè)副本膏执,所以會(huì)符合我們的預(yù)期輸出不同的值。

函數(shù)按值傳遞

函數(shù)傳參就兩個(gè)類型残揉,基本類型和引用類型胧后,大家糾結(jié)的都是引用類型的傳遞芋浮。

引用類型作為參數(shù)傳入函數(shù)抱环,傳的是個(gè)地址值,或者指針值纸巷,不是那個(gè)引用類型本身镇草,它還好好的呆在堆內(nèi)存呢。賦值給argument的同樣是地址值或者指針瘤旨。所以說(shuō)是value值傳遞一點(diǎn)沒(méi)錯(cuò)梯啤,傳的是個(gè)地址值。通過(guò)兩個(gè)例子看懂就行了存哲。

例子1:

function setName(obj) {
obj.name = 'aaa';
var obj = new Object(); // 如果是按引用傳遞的,此處傳參進(jìn)來(lái)obj應(yīng)該被重新引用新的內(nèi)存單元
obj.name = 'ccc';
return obj;
}

var person = new Object();
person.name = 'bbb';
var newPerson = setName(person);
console.log(person.name + ' | ' + newPerson.name); // aaa | ccc

從結(jié)果看因宇,并沒(méi)有顯示兩個(gè)'ccc'。這里是函數(shù)內(nèi)部重寫(xiě)了obj祟偷,重寫(xiě)的obj是一個(gè)局部對(duì)象察滑。當(dāng)函數(shù)執(zhí)行完后,立即被銷毀修肠。

引用值:對(duì)象變量它里面的值是這個(gè)對(duì)象在堆內(nèi)存中的內(nèi)存地址贺辰。因此如果按引用傳遞,它傳遞的值也就是這個(gè)內(nèi)存地址嵌施。那么var obj = new Object();會(huì)重新給obj分配一個(gè)地址饲化,比如是0x321了,那么它就不在指向有name = 'aaa';屬性的內(nèi)存單元了吗伤。相當(dāng)于把實(shí)參obj和形參obj的地址都改了吃靠,那么最終就是輸出兩個(gè)ccc了。

例子2

var a = {
num:'1'
};

var b = {
num:'2'
};

function change(obj){
obj.num = '3';
obj = b;
return obj.num;
}

var result = change(a);
console.log(result + ' | ' + a.num); // 2 | 3

首先把a(bǔ)的值傳到change函數(shù)內(nèi)足淆,obj.num = '3';后a.name被修改為3;
a的地址被換成b的地址;
返回此時(shí)的a中a.num巢块。

閉包中使用this對(duì)象

概念

this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的:全局函數(shù)中,this等于window;當(dāng)函數(shù)被作用某個(gè)對(duì)象的方法調(diào)用時(shí)缸浦,this等于那個(gè)對(duì)象夕冲。

但在匿名函數(shù)中,由于匿名函數(shù)的執(zhí)行環(huán)境具有全局性裂逐,因此this對(duì)象通常指向window(在通過(guò)call或apply函數(shù)改變函數(shù)執(zhí)行環(huán)境的情況下歹鱼,會(huì)指向其他對(duì)象)。

var name = "The Window";
        
        var object = {
            name : "My Object",
        
            getNameFunc : function(){
                return function(){
                    return this.name;
                };
            }
        };
        
        alert(object.getNameFunc()());  //"The Window"

通過(guò)修改把作用域中的this對(duì)象保存在一個(gè)閉包能夠訪問(wèn)到的變量里卜高,就可以讓閉包訪問(wèn)該對(duì)象了弥姻。如下代碼:

var name = "The Window";
            
            var object = {
                name : "My Object",
            
                getNameFunc : function(){
                    var that = this;
                    return function(){
                        return that.name;
                    };
                }
            };
            
            alert(object.getNameFunc()());  //"MyObject"

變量聲明提前

var scope="global";  

function scopeTest() { 
    console.log(scope);  
    var scope="local";  
}  
scopeTest(); //undefined

此處的輸出是undefined南片,并沒(méi)有報(bào)錯(cuò),這是因?yàn)樵谇懊嫖覀兲岬降暮瘮?shù)內(nèi)的聲明在函數(shù)體內(nèi)始終可見(jiàn)庭敦,上面的函數(shù)等效于:

var scope="global";  
function scopeTest() {  
    var scope;  
    console.log(scope);  
    scope="local";  
}  
scopeTest(); //undefined

注意疼进,如果忘記var,那么變量就被聲明為全局變量了秧廉。結(jié)果就是global

沒(méi)有塊級(jí)作用域

和其他我們常用的語(yǔ)言不同伞广,在Javascript中沒(méi)有塊級(jí)作用域:

 function scopeTest() {
            var scope = {};
            if (scope instanceof Object) {
                var j = 1;
                for (var i = 0; i < 10; i++) {
                    console.log(i); //輸出0-9
                }
                console.log(i); //輸出10 
            }
            console.log(j); //輸出1 
        }
        scopeTest();

在javascript中變量的作用范圍是函數(shù)級(jí)的,即在函數(shù)中所有的變量在整個(gè)函數(shù)中都有定義疼电,這也帶來(lái)了一些我們稍不注意就會(huì)碰到的“潛規(guī)則”:

var scope = "hello"; 
function scopeTest() { 
    console.log(scope);//① 
    var scope = "no"; 
    console.log(scope);//② 
}

在①處輸出的值竟然是undefined嚼锄,簡(jiǎn)直喪心病狂啊,我們已經(jīng)定義了全局變量的值啊蔽豺,這地方不應(yīng)該為hello嗎区丑?其實(shí),上面的代碼等效于:

var scope = "hello"; 
function scopeTest() { 
    var scope; 
    console.log(scope);//① 
    scope = "no"; 
    console.log(scope);//② 
}

聲明提前修陡、全局變量?jī)?yōu)先級(jí)低于局部變量沧侥,根據(jù)這兩條規(guī)則就不難理解為什么輸出undefined了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末魄鸦,一起剝皮案震驚了整個(gè)濱河市宴杀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌号杏,老刑警劉巖婴氮,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異盾致,居然都是意外死亡主经,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)庭惜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罩驻,“玉大人,你說(shuō)我怎么就攤上這事护赊』荻簦” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵骏啰,是天一觀的道長(zhǎng)节吮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)判耕,這世上最難降的妖魔是什么透绩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上帚豪,老公的妹妹穿的比我還像新娘碳竟。我一直安慰自己,他們只是感情好狸臣,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布莹桅。 她就那樣靜靜地躺著,像睡著了一般烛亦。 火紅的嫁衣襯著肌膚如雪诈泼。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天此洲,我揣著相機(jī)與錄音厂汗,去河邊找鬼。 笑死呜师,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贾节。 我是一名探鬼主播汁汗,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼栗涂!你這毒婦竟也來(lái)了知牌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤斤程,失蹤者是張志新(化名)和其女友劉穎角寸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體忿墅,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扁藕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疚脐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亿柑。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖棍弄,靈堂內(nèi)的尸體忽然破棺而出望薄,到底是詐尸還是另有隱情,我是刑警寧澤呼畸,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布痕支,位于F島的核電站,受9級(jí)特大地震影響蛮原,放射性物質(zhì)發(fā)生泄漏卧须。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望故慈。 院中可真熱鬧板熊,春花似錦、人聲如沸察绷。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拆撼。三九已至容劳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間闸度,已是汗流浹背竭贩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留莺禁,地道東北人留量。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像哟冬,于是被迫代替她去往敵國(guó)和親楼熄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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