javascript學習-閉包

javascript學習-閉包

1.什麼是閉包

大多數書本中對閉包的定義是:「閉包是指有權訪問另外一個函數做用域中的變量的函數。」。這個概念過於抽象了,對初學者而言沒啥幫助。好在《Javascript忍者祕籍》5.1中給了一個例子來進一步的解釋了什麼是閉包:javascript

            var outerValue= 'ninja';
            
            var later;
            
            function outerFunction() {
                var innerValue = "samurai";
                
                function innerFunction(paramValue) {
                    assert(outerValue == "ninja", "I can see the outerValue.");
                    assert(innerValue == "samurai", "I can see the innerValue.");
                    assert(paramValue == "wakizashi", "I can see the paramValue.");
                    assert(tooLater == "ronin", "Inner can see the tooLater.");
                }
                
                later = innerFunction;
            }
            
            assert(tooLater, "Outer can't see the tooLater.");
            
            var tooLater = "ronin";
            
            outerFunction();
            
            later("wakizashi");

測試結果是:前端

看,這個later指向的就是一個閉包,它實際指向了一個外部函數outerFunction中的一個內部函數innerFunction。當outerFunction函數被調用經過全局變量later將innerFunction函數從outerFunction函數這個封閉的監獄裏放出來後,innerFunction函數就一會兒變得超級厲害了,成爲了閉包,一旦調用閉包,它既能看見全局的outerValue,監獄裏的innerValue,本身隨身攜帶的paramValue,還能看見之前根本不認識的tooLater。java

固然了,我認爲這裏例子並不完整,爲了形式的完整性,我給它增強一下:緩存

            asserts();

            test("函數閉包", function() {
                var before_outerFunction = "before_outerFunction";

                function outerFunction(outerParam) {
                    var before_innerFunction = "before_innerFunction";

                    function innerFunction(innerParam) {
                        return {
                            before_outerFunction : before_outerFunction,
                            after_outerFunction : after_outerFunction,
                            before_innerFunction : before_innerFunction,
                            after_innerFunction : after_innerFunction,
                            outerParam : outerParam,
                            innerParam : innerParam,
                            before_callClosure : before_callClosure,
                            after_callClosure : after_callClosure,
                        };
                    }

                    var after_innerFunction = "after_innerFunction";
                    
                    return innerFunction;
                }

                var after_outerFunction = "after_outerFunction";

                var closure = outerFunction("outerParam");

                var before_callClosure = "before_callClosure";

                var ret = closure("innerParam");
                
                assert(ret.before_outerFunction, "before_outerFunction");
                assert(ret.after_outerFunction, "after_outerFunction");
                assert(ret.before_innerFunction, "before_innerFunction");
                assert(ret.after_innerFunction, "after_innerFunction");
                assert(ret.outerParam, "outerParam");
                assert(ret.innerParam, "innerParam");
                assert(ret.before_callClosure, "before_callClosure");
                assert(ret.after_callClosure, "after_callClosure");

                var after_callClosure = "after_callClosure";
            });

測試結果是:閉包

結論就是,當閉包被調用的那一刻,它立刻就立地成佛了,既能看到眼前看到的,也能看到曾今看到的,前世此生,形形色色,全都歷歷在目啊。只有還沒有發生的after_callClosure,那個實在是看不到。函數

難怪不止一本書中提到,只有理解了閉包才能真正的理解Javascript,這玩意就是一個反直覺的異類啊。性能

2.函數做用域鏈

若是隻是認識一下什麼是閉包,那上面的一段就夠了,可是這遠遠稱不上理解閉包。《Javascript忍者祕籍》在這裏及其不負責任的開始大講特講怎麼使用閉包:學習

  • 用閉包實現私有變量
  • 在回調函數中使用閉包
  • 在定時器中使用閉包
  • 用閉包實現函數的bind
  • 用閉包實現函數的curry化
  • 用閉包實現函數結果的緩存
  • 用閉包實現函數的包裝

這個忍者師傅估計當時是喝多了,簡單露了兩手後就扔給咱們一堆的掌法、步法、劍法、刀法。惟獨把最關鍵的內功心法給忘了。好了,不期望它了,仍是本身接着找師傅吧,有錢就是任性,請了一堆的師傅在桌上擺着,因此才這麼有底氣。因而我看到了《Javascript高級程序設計》這位牛逼的不行的師傅。高手就是高手,一上來就告訴我,要理解閉包,先翻到第四章看看什麼是做用域鏈。立馬翻過去看啊,4.2關於執行環境及做用域,只有短短的兩頁,大致意思是:經過執行環境、做用域鏈、活動對象,咱們實現了變量的一層層查找。得了,我智商不夠,繼續換老師,因而我又找到了《高性能Javascript》,第二章中的一小段,題目是「做用域鏈和標識符解析」,一樣是短短的兩頁,字字珠璣,圖文並茂,立馬有種醍醐灌頂的感受。測試

每個Javascript函數都表示爲一個對象,更確切的說,是Function對象的一個實例。this

當編譯器看到下面的全局函數:

                function add(num1, num2) {
                    var sum = num1 + num2;
                    return sum;
                }

它經過相似下面的代碼來建立函數對象:

var add = new Function("num1", "num2", "var sum = num1 + num2;\nreturn sum;");

Function函數中要自動完成做用域鏈的構造:

  1. 建立add函數對象的做用域鏈對象,並把引用保存在add函數對象的[[Scope]]屬性中
  2. 把做用域鏈對象的第一項指向全局做用域對象

聽起來是蠻複雜,用下面的圖形象化一下:

當add被調用時,例如經過下面的代碼:

var total = add(5, 10);

此次輪到引擎來幹活了,它要完成下面的工做:

  1. 執行此函數會建立一個稱爲執行環境(execution context)的內部對象。一個執行環境定義了一個函數執行時的環境。函數每次執行時都對應的執行環境都是臨時的,因此屢次調用同一個函數就會致使建立多個執行環境。當函數執行完畢,執行環境就被銷燬。
  2. 每一個執行環境都有本身的做用域鏈,用於解析標識符。當執行環境被建立時,它的做用域鏈初始化爲當前運行函數的[[Scope]]屬性中的對象。這些值按照它們出如今函數中的順序,被複制到執行環境的做用域鏈中。
  3. 這個過程一旦完成,一個被稱爲「活動對象」(activation object)的新對象就爲執行環境建立好了。活動對象做爲函數運行時的變量對象,包含了此函數的全部局部變量,命名參數,參數集合以及this。
  4. 而後活動對象被推入到執行環境做用域鏈的最前端。
  5. 函數執行過程當中每次解析標識符,就在執行環境的做用域鏈中從前日後的查找
  6. 函數執行完畢,執行環境被銷燬,活動對象也隨之被銷燬。

再用書上的圖形象化一下:

3.閉包與函數做用域鏈

仍然是《高性能Javascript》,第二章其中的一小段,題目是「閉包、做用域和內存」。給出的示例代碼以下:

                function saveDocument(id) {                    
                }

                function assignEvents() {
                    var id = "xdi9592";
                    document.getElementById("save-btn").onclick = function(event) {
                        saveDocument(id);
                    }
                }

assignEvents()函數給一個DOM元素設置事件處理函數。這個事件處理函數就是一個閉包,它在assignEvents()執行時建立,而且能訪問所屬做用域的id變量。爲了讓這個閉包訪問id,必須建立一個特定的做用域鏈。

當assignEvents()函數執行時,一個包含了變量id以及其餘數據的活動對象被建立。它成爲執行環境做用域鏈中的第一個對象,而全局對象緊隨其後。當閉包被建立時,它的[[Scope]]屬性被初始化爲這些對象。

因爲閉包的[[Scope]]屬性包含了與執行環境做用域鏈相同的的對象的引用,所以會產生反作用。一般來講,函數的活動對象會隨着執行環境一同銷燬。但引入閉包時,因爲引用仍然存在於閉包的[[Scope]]屬性中,所以激活對象沒法被銷燬。

當閉包被執行時,會建立一個執行環境,它的做用域鏈與屬性[[Scope]]中所引用的兩個相同的做用域鏈對象一塊兒被初始化,而後一個活動對象爲閉包自身所建立。

 

《Javascript高級程序設計》也有一個相似的例子,示例代碼以下:

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

                //建立函數
                var compareNames = createComparisonFunction("name");
                //調用函數
                var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
                //解除對匿名函數的引用(以便釋放內存)
                compareNames = null;

函數compareNames被調用時的執行環境以下圖:

這張圖其實很容易引發誤解,不少人覺得這張圖是函數compareNames被調用時的一個快照,其實不是,當createComparisonFunction函數執行時,createComparisonFunction函數的執行環境是沒錯的。可是它返回的那個匿名函數是一個閉包,所以該匿名函數的做用域鏈會複製createComparisonFunction函數的執行環境的做用域鏈,而後createComparisonFunction函數結束,createComparisonFunction函數的執行環境被銷燬,可是createComparisonFunction函數的活動對象由於被閉包引用了,因此沒法銷燬。

當compareNames執行時,它依然遵循着普通函數的執行流程:

  1. 建立函數的執行環境;
  2. 建立函數的執行環境的做用域鏈,並複製函數的做用域鏈;
  3. 建立函數的活動對象,並加入到函數的執行環境的做用域鏈的第一條

4.全局對象是什麼

這個問題提的彷佛很沒有水平啊,學習Javascript的初學者哪一個不知道全局對象的重要性呢。可是若是換一個角度來看,若是把全部Javascript代碼認爲是寫在一個最外層的超級函數裏的。那麼當這個超級函數執行時,它應該也會繼續函數調用的三板斧:

  1. 建立超級函數的執行環境;
  2. 建立超級函數的執行環境的做用域鏈,並複製超級函數的做用域鏈,此時爲空;
  3. 建立超級函數的活動對象,並加入到超級函數的執行環境的做用域鏈的第一條

根據函數做用域鏈的檢索機制和全局對象的用法,咱們彷佛能夠獲得一個推論:全局對象其實就是超級函數的活動對象。

再進一步,咱們定義的全部全局函數其實都是閉包,由於它們都把超級函數的活動對象,也就是全局函數複製到了本身的函數做用域鏈中。

再次回到咱們開頭對閉包的測試用例,若是咱們不是直接返回內部函數,而是直接在外部函數裏調用內部函數呢?

            test("函數閉包", function() {
                var before_outerFunction = "before_outerFunction";

                function outerFunction(outerParam) {
                    var before_innerFunction = "before_innerFunction";

                    function innerFunction(innerParam) {
                        return {
                            before_outerFunction : before_outerFunction,
                            after_outerFunction : after_outerFunction,
                            before_innerFunction : before_innerFunction,
                            after_innerFunction : after_innerFunction,
                            outerParam : outerParam,
                            innerParam : innerParam,
                            before_callClosure : before_callClosure,
                            after_callClosure : after_callClosure,
                        };
                    }

                    var after_innerFunction = "after_innerFunction";

                    var ret = innerFunction("xxx");
                    assert(ret.before_outerFunction, "before_outerFunction");
                    assert(ret.after_outerFunction, "after_outerFunction");
                    assert(ret.before_innerFunction, "before_innerFunction");
                    assert(ret.after_innerFunction, "after_innerFunction");
                    assert(ret.outerParam, "outerParam");
                    assert(ret.innerParam, "innerParam");
                    assert(ret.before_callClosure, "before_callClosure");
                    assert(ret.after_callClosure, "after_callClosure");
                    
                    return innerFunction;
                }

                var after_outerFunction = "after_outerFunction";

                var closure = outerFunction("outerParam");
                //log(closure);

                var before_callClosure = "before_callClosure";

                var ret = closure("innerParam");

                assert(ret.before_outerFunction, "before_outerFunction");
                assert(ret.after_outerFunction, "after_outerFunction");
                assert(ret.before_innerFunction, "before_innerFunction");
                assert(ret.after_innerFunction, "after_innerFunction");
                assert(ret.outerParam, "outerParam");
                assert(ret.innerParam, "innerParam");
                assert(ret.before_callClosure, "before_callClosure");
                assert(ret.after_callClosure, "after_callClosure");

                var after_callClosure = "after_callClosure";
            });

測試結果是:

一切和預想的同樣,所謂的閉包並非return是才發生的,而是在內部函數被編譯器建立函數對象的那一刻就決定的。爲函數的做用域鏈複製當前函數執行環境的做用域鏈。用下圖形象化一下:

最關鍵的是這套函數定義、函數調用機制是能夠無限嵌套下去的,並且函數定義和函數調用的時機也是分離的。每一個函數執行時都有本身的活動對象負責管理本身的做用域,執行環境做用域鏈的職責不過是把這些嵌套的函數的各自的活動對象串聯起來。函數的做用域鏈的職責不過是至關於一箇中間變量,負責保存上一級函數執行環境的做用域鏈。

因此內部函數直接在外部函數內調用的話也能訪問到內部函數的相關變量,不是由於內部函數調用時真的能夠看見外部函數,而是由於內部函數的執行環境的做用域鏈中已經複製了外部函數的執行環境的做用域鏈。函數的執行環境的做用域鏈是自完備的,函數調用時只會在本身的函數的執行環境的做用域鏈中查找,其實它根本就不知道外部函數或者全局函數什麼的。

若是內部函數不return出去的話,一切都會隨着外部函數調用完成,外部函數的執行環境對象被銷燬,致使外部函數的執行環境做用域鏈的被銷燬,致使外部函數活動對象被銷燬,與此同時內部函數對象做爲局部變量也會被銷燬。這一系列的銷燬過程將內部函數的做用域鏈複製了外部函數的執行環境的做用域鏈的「罪證」被掩蓋得完美無缺。臨時的外部函數的活動對象也絕對不會跑到籠子外面去。

可是一旦內部函數被return出去的話,內部函數的做用域鏈複製了外部函數的執行環境的做用域鏈的「罪證」被暴露了,本該被銷燬的外部函數的活動對象也意外地活了下來,並隨時等待着隨着內部函數被調用而繼續呼風喚雨。其實不只僅是外部函數的活動對象,外部函數的執行環境的做用域鏈上的全部活動對象都意外的活了下來,若是咱們構造一個二層以上的函數嵌套,不斷地進行函數的定義和函數的調用,最後返回一個最內層的函數,你就會發現一組本該死去的活動對象都意外的活了下來。

閉包執行後直接設置爲null能夠保證那些意外活下來的活動對象被清除嗎?按道理應該是這樣的,不過很不肯定,這取決於引擎的垃圾回收機制怎麼玩的,若是按照引用計數的垃圾收集方式,這個推論應該是真的,可是目前大多數引擎採用的倒是標記清除的垃圾收集方式,這就很難保證這個推論是真的了。或者更簡單的,隨着閉包引用變量的自動清除,也能讓那些活動對象壽終正寢,也是說得過去的。這或許也正好解釋了看到的Javascript代碼中不多有對閉包主動設置爲null的。

在Javascript的世界裏,其設計思想果真仍是一如既往的單純質樸。

如何管理函數?Javascript回答說用函數對象。

如何管理函數的做用域?Javascript回答說用活動對象。

若是函數調用有嵌套呢?Javascript回答說用做用域鏈,把活動對象串起來。

若是一個外部函數返回了一個內部函數,致使外部函數的活動對象泄露了怎麼辦?Javascript回答說那就叫作閉包吧。

 5.後記

本文的一切功勞屬於那些經典書籍的做者們,我不生產知識,我只是知識的搬運工。本文的一切錯誤屬於我我的,誰讓我是初學者呢,有時候搬錯了也是不免的,那就在不斷地錯誤不斷地改正中不斷地成長吧。

相關文章
相關標籤/搜索