JavaScript高級程序設計——閉包

js-closure

前言

有很多人搞不清匿名函數閉包這兩個概念,經?;煊?。閉包是指有權訪問另一個函數作用域中的變量的函數。匿名函數就是沒有實際名字的函數。

閉包

概念

閉包,其實是一種語言特性,它是指的是程序設計語言中,允許將函數看作對象,然后能像在對象中的操作搬在函數中定義實例(局部)變量,而這些變量能在函數中保存到函數的實例對象銷毀為止,其它代碼塊能通過某種方式獲取這些實例(局部)變量的值并進行應用擴展。

條件

閉包是允許函數訪問局部作用域之外的數據。即使外部函數已經退出,外部函數的變量仍可以被內部函數訪問到。

因此閉包的實現需要三個條件:

內部函數實用了外部函數的變量
外部函數已經退出
內部函數可以訪問

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

上述代碼在執行的時候,b得到的是閉包對象的引用,雖然a執行完畢后,但是a的活動對象由于閉包的存在并沒有被銷毀,在執行b(1)的時候,仍然訪問到了x變量,并將其加1,若再執行b(1),則x是2,因為閉包的引用b并沒有消除。(后面會解釋,閉包返回了函數,函數可以創建獨立的作用域)

閉包,其實就是指程序語言中能讓代碼調用已運行的函數中所定義的局部變量。

但是你只需要知道應用的兩種情況即可——函數作為返回值,函數作為參數傳遞。

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

如上代碼,bar函數作為返回值,賦值給f1變量。執行f1(15)時,用到了fn作用域下的max變量的值。至于如何跨作用域取值,可以參考上一篇文章。

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

如上代碼中,fn函數作為一個參數被傳遞進入另一個函數,賦值給f參數。執行f(15)時,max變量的取值是10,而不是100。

上一篇講到自由變量跨作用域取值時,曾經強調過:要去創建這個函數的作用域取值,而不是“父作用域”。理解了這一點,以上兩端代碼中,自由變量如何取值應該比較簡單.

另外,講到閉包,除了結合著作用域之外,還需要結合著執行上下文棧來說一下。

在前面講執行上下文棧時,我們提到當一個函數被調用完成之后,其執行上下文環境將被銷毀,其中的變量也會被同時銷毀。

有些情況下,函數調用完成之后,其執行上下文環境不會接著被銷毀。這就是需要理解閉包的核心內容。

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

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);

全局作用域為:代碼1-12行;fn作用域為:代碼2-10行;bar作用域為:代碼5-9行。


舉例

第一步,代碼執行前生成全局上下文環境,并在執行時對其中的變量進行賦值。此時全局上下文環境是活動狀態。

js-closure

第二步,執行第17行代碼時,調用fn(),產生fn()執行上下文環境,壓棧,并設置為活動狀態。


js-closure

第三步,執行完第17行,fn()調用完成。按理說應該銷毀掉fn()的執行上下文環境,但是這里不能這么做。注意,重點來了:

因為執行fn()時,返回的是一個函數。函數的特別之處在于可以創建一個獨立的作用域。而正巧合的是,返回的這個函數體中,還有一個自由變量max要引用fn作用域下的fn()上下文環境中的max。因此,這個max不能被銷毀,銷毀了之后bar函數中的max就找不到值了。

因此,這里的fn()上下文環境不能被銷毀,還依然存在與執行上下文棧中。

——即,執行到第18行時,全局上下文環境將變為活動狀態,但是fn()上下文環境依然會在執行上下文棧中。另外,執行完第18行,全局上下文環境中的max被賦值為100。如下圖:


js-closure

第四步,執行到第20行,執行f1(15),即執行bar(15),創建bar(15)上下文環境,并將其設置為活動狀態。


js-closure

執行bar(15)時,max是自由變量,需要向創建bar函數的作用域中查找,找到了max的值為10。這個過程在作用域鏈一節已經講過。

這里的重點就在于,創建bar函數是在執行fn()時創建的。fn()早就執行結束了,但是fn()執行上下文環境還存在與棧中,因此bar(15)時,max可以查找到。如果fn()上下文環境銷毀了,那么max就找不到了。

總結:使用閉包會增加內容開銷

第五步,執行完20行就是上下文環境的銷毀過程,這里就不再贅述了。

閉包與變量

概念

閉包只能取得包含函數中任何變量的最后一個值,閉包所保存的是整個變量對象,而不是某個特殊變量。

例子

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

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

            return result;
        }

        var funcs = createFunctions();

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

總結:每個函數的作用域鏈中都保存著createFunctions()函數的活動對象,所以它們引用的都是同一個變量i。當createFunctions()函數返回后,變量i的值為10。

我們可以通過創建另一個匿名函數強制讓閉包的行為符合預期。

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();

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

總結:沒有直接把閉包賦值給數組,而是定義了一個匿名函數,并通過立即執行該匿名函數的結果賦值給數組,并帶了for循環的參數i進去,讓x能找到傳入的參數值為0-10,這就解釋了函數參數是按值傳遞的,所以會將變量i的當前值復制給參數x。而這個匿名函數內部又創建并返回了一個訪問x的閉包。這樣以來result數組中的每個函數都有自己x變量的一個副本,所以會符合我們的預期輸出不同的值。

函數按值傳遞

函數傳參就兩個類型,基本類型和引用類型,大家糾結的都是引用類型的傳遞。

引用類型作為參數傳入函數,傳的是個地址值,或者指針值,不是那個引用類型本身,它還好好的呆在堆內存呢。賦值給argument的同樣是地址值或者指針。所以說是value值傳遞一點沒錯,傳的是個地址值。通過兩個例子看懂就行了。

例子1:

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

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

從結果看,并沒有顯示兩個'ccc'。這里是函數內部重寫了obj,重寫的obj是一個局部對象。當函數執行完后,立即被銷毀。

引用值:對象變量它里面的值是這個對象在堆內存中的內存地址。因此如果按引用傳遞,它傳遞的值也就是這個內存地址。那么var obj = new Object();會重新給obj分配一個地址,比如是0x321了,那么它就不在指向有name = 'aaa';屬性的內存單元了。相當于把實參obj和形參obj的地址都改了,那么最終就是輸出兩個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的值傳到change函數內,obj.num = '3';后a.name被修改為3;
a的地址被換成b的地址;
返回此時的a中a.num。

閉包中使用this對象

概念

this對象是在運行時基于函數的執行環境綁定的:全局函數中,this等于window;當函數被作用某個對象的方法調用時,this等于那個對象。

但在匿名函數中,由于匿名函數的執行環境具有全局性,因此this對象通常指向window(在通過call或apply函數改變函數執行環境的情況下,會指向其他對象)。

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

通過修改把作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了。如下代碼:

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,并沒有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見,上面的函數等效于:

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

注意,如果忘記var,那么變量就被聲明為全局變量了。結果就是global

沒有塊級作用域

和其他我們常用的語言不同,在Javascript中沒有塊級作用域:

 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中變量的作用范圍是函數級的,即在函數中所有的變量在整個函數中都有定義,這也帶來了一些我們稍不注意就會碰到的“潛規則”:

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

在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全局變量的值啊,這地方不應該為hello嗎?其實,上面的代碼等效于:

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

聲明提前、全局變量優先級低于局部變量,根據這兩條規則就不難理解為什么輸出undefined了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容