對js閉包的粗淺理解

  只能是粗淺的,畢竟js用法太靈活。編程

  首先拋概念:閉包(closure)是函數對象與變量做用域鏈在某種形式上的關聯,是一種對變量的獲取機制。這樣寫鬼能看懂。數組

      因此要大體搞清三個東西:函數對象(function object)、做用域鏈(scope chain)以及它們如何關聯(combination)瀏覽器

   首先要創建一個印象,在js中,幾乎全部的東西能夠看做對象,除了null和undefined。好比經常使用的數組對象、日期對象、正則對象等。閉包

    var num = 123; // Number
    var arr = [1,2,3]; // Array
    var str = "hello";  // String

  數字原始值能夠看做Number對象,字符串原始值可看作String對象,數組原始值可看做Array對象。有的原始值還可直接調方法,如數組、正則,有的不行編程語言

    [1,2,3].toString();  // 能夠
    /\w+/.toString();  // 能夠
    123.toString();  // 報錯

  好比數字,當用一個變量名呈接它時,或者加上括號,可以這樣用函數

    var num = 123;
    num.toString();
    123.toString();  // 報錯,因解釋器當成浮點數來解讀
    (123).toString();  // 能夠

  因此,函數也能夠是一個對象,函數細提及來又要扯一大堆,畢竟是js精華,簡單說這裏有用的。函數的定義常見的是this

    function fun1(val){  }
    fun1(5);  

    var f1 = function(val){  };  // 定義一個函數賦給變量
    f1(6);
    var f2 = function fun2(){ }; // 或者取一個函數名
    f2();

  由於函數也是對象、變量,因此能夠賦給一個變量(更準確說是賦給左值),在這個變量後面使用()調用運算符就能夠調用這個函數了,如f1()。還有一種方式:定義即調用google

    var num = (function(val){ return val * val; }(5)); // num爲25

  在定義一個函數體後面加上()和參數(或者沒有參數),就是對這個定義的函數進行了調用,直接傳入參數5計算並賦值給num,所以num是一個數值變量而不是函數變量。spa

      既然函數是對象,固然也有new關鍵字的表達式code

    var a = new Array(1,2,3);  // 數組的對象建立表達式
    var f = new Function("x", "y", "x = 2 *x; return x + y;");  // 函數對象建立表達式
    
    var f1 = function(x, y){  // 函數f的函數體相似f1的定義
          x = 2 * x;
          return x + y;
    };

  函數對象的建立使用了Function關鍵字,前面的參數均被當作對象建立函數的形參,如這裏的x、y,最後一個字符串是函數的函數體,多行函數體仍以;相隔。

     不少時候特別是使用jQuery時常常看到函數調用時傳遞函數,大多數時候直接寫匿名函數,也可可傳遞一個函數變量

    func(index, function(val){
           /* 匿名函數 */
    });
    var f = function(val){ /* 函數變量 */ };
   func1(index, f);

  既然函數函數是對象,能夠賦給一個變量,天然也能夠做爲返回值了,並且在js中,函數能夠嵌套定義。

    function func(){
        return function(x){  // 返回一個函數變量
            return x * x;
        }
    }
    var f = func();
    f(5);  // 對這個函數進行調用

function func1(){ function nested1(){ } // 嵌套定義函數 function nested2(){ } }

   對函數有個大體瞭解,說說變量做用域問題,有幾個原則:

       1. 全局變量擁有全局做用域.

       2. 局部變量(通常指定義在函數內部)擁有局部做用域(包括其嵌套的函數).

       3. 在局部變量若跟全局變量重名,優先使用局部變量.

    val = 'value';  // 變量定義能夠不用var關鍵字
    document.write("val=>" + val + "<br/>");

    // g是全局變量,在全局做用域中有效,因此在給g初始化以前就能夠訪問,只是值是undefined
    document.write("g=>" + g );
    var g = 'google';

  全局變量的定義,至關因而全局對象的屬性,通常咱們用this指代這個全局對象,好比在瀏覽器中運行的時候,它指的是window對象,即當前窗體,在全局做用域中,如下三種訪問形式等效

    var g = 'google';
    document.write("g=>" + g + "<br/>");
    document.write("g=>" + this.g + "<br/>");
    document.write("g=>" + window.g + "<br/>");

   而在函數定義內部訪問變量時,遵循同名優先,函數做用域內部老是優先訪問,例如

    var scope = "global";
    function func1(){
        var scope = "local";
        console.log(scope);  // local
    };
    func1();
    function func2(){
        scope = 'changed global';  // 如不加var,改變的是全局變量的值
    };
    func2();
    console.log(scope);  // changed global

  比較有意思的地方是,若是在一個函數內部給一個全局變量賦值時沒有加var關鍵字,如func2,它改變的是全局做用域變量的值!而前面說的this,也有個有意思的地方

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

  在局部做用域(這裏均指函數內部),若是有同名變量,以this引用的話,結果是全局變量,即,在函數內部(注意不是方法,方法通常指對象的屬性方法),this指代的是全局的對象。再看一個

    var scope = "global";
    function func(){
        "use strict";   // 開啓ECMAScript5嚴格模式
        console.log(this);  // undefined
        console.log(this.scope);  // 報錯:TypeError: scope undefined
    }
    func();

  在嚴格模式中,對語法檢查更加嚴格。在一個全局做用域定義的普通函數中,log打印this是undefined,因此this引用scope固然也不存在。

      在ECMAScript(爲js腳本制定的一個標準)中,強制規定全局做用域定義的變量,是全局對象(好比在瀏覽器客戶端運行時爲window,默認的全局中的this關鍵字也是指它,一般咱們說的就是這個)的屬性。通常咱們把跟某個變量做用域相關的對象爲上下文環境(context),好比全局對象this只要涉及環境這類偏底層的東西確定就是編程語言層面本身規定的。

  但這個全局對象this限於非嚴格模式的狀況。js容許咱們在局部變量的環境中(函數)以this引用全局對象,在嚴格模式下卻無法這樣幹,也許是它把這個對象給隱藏了,至少目前這樣寫會報錯。

  正是js的函數有局部做用域的特殊功能---全局做用域沒法訪問函數中定義的變量,因此在js中,也用函數規定命名空間,相比其餘有的語言使用的是namespace關鍵字,js目前好像是把namespace做爲保留字,你的變量的命令不能跟它重名,但沒投入使用。

    (function(){
        var name = "Jeff";
        var age = 28;
        var pos = "development";
    }());  // 在函數中定義一堆變量,外邊沒法訪問

  在一個函數中定義一堆變量,固然得調用它才能生效,這堆變量就限於在這個空間內使用了,好像用得也很少。

  一個重要的點,一個函數中規定的變量,在這個函數內部全部地方均可訪問,包括嵌套函數。因此能夠出現下面這個現象

    function func(){
        console.log(num);  // undefined,先於定義訪問
        var num = 123;
        console.log(num);  // 123
    }
    func();

  這種特性有時被稱爲聲明提早(hoisting),至關於這樣

    function func(){
        var num;
        console.log(num);  // undefined,先於定義訪問
        var num = 123;
        console.log(num);  // 123
    }

  而後是嵌套函數,只要在函數內定義的,該函數內均能訪問,看例子

    function func(){
        var str = "hello";
        function nested1(){  // 第一次嵌套
            str = "nested1";
            function nested2(){ str = "nested2"; }  // 第二次嵌套
            nested2();
        }
        nested1();
        console.log(str);
    }
    func();  // 打印nested2

   除了明確在函數內定義的變量,還有定義函數時的形參,它們在整個函數內也是可訪問的。

  如今咱們大概瞭解函數也是對象,以及全局、局部做用域,在全局做用域定義的變量,是對應的全局對象的屬性,也就是說有個全局對象關聯它,就從這點進入做用域鏈(scope chain)吧,這是理解閉包的基礎。

  在一個局部做用域內,或者說定義的函數,想象它們關聯着某個對象,這個對象是隨着咱們定義這個函數而自動生成的,函數內定義的變量以及函數的形參均是這個對象的屬性,因此在這個對象內部老是能夠順利訪問到它們,相似於全局變量是全局對象的屬性。這種對象關聯在定義時就已經決定了,而不是在調用時才造成(這很重要)。

      可是咱們定義的不少函數都是嵌套的,由外到內每一個函數都會有一個對應的自定義對象跟它關聯

    var a = "a";
    var name = "Michel";
    function fun(b){
        var c = "c";
        var name = "Clark";
        function nested(d){
            var name = "Bruce";
            var e = "e";
            /* TODO */
        }
        /* TODO */
    }

  在上面的嵌套函數nested生成一個自定義對象時,fun函數、全局做用域也會生成對象,所以它們能夠造成一個對象的列表或者鏈表,簡單的將函數名做爲對象名,全局對象用global表示,而且從內嵌函數nested函數出發的話,大概是這樣:

    

  列表上的一組對象定義了這段代碼做用域中定義過的變量:即它們的屬性。第一個對象的屬性是當前函數的形參與內定義變量,第二個對象是它的外部函數的形參與內定義變量,一直到最後是全局對象和它的屬性---全局定義的變量,也就是說,當前函數永遠在這個列表的最前面,這樣才能夠保證該函數範圍內的變量老是具備最訪問高優先級。每次訪問變量時便會順着這個列表查找,這被稱爲變量解析(variable resolution),若是一直找到列表末尾都找不到對象中的這個屬性,會拋一個ReferenceError錯誤。

  每一個定義的函數對象都會有相似這樣一個列表與之關聯,它們之間經過這個做用域鏈相關聯,而函數體內定義的變量都可保存在函數做用域內,這是在函數定義時及肯定的,這種特性稱之爲閉包。      一般直接把函數稱做閉包,並且理論上來講全部的js函數都是閉包,由於它們都是對象,閉包這種機制讓js有能力來「捕捉」變量。第一個例子:

    var scope = "global";
    function func(){
        var scope = "local";
        function nested(){ return scope; }
        return nested();  // 調用並返回scope
    }
    console.log(func());

  常規的定義和調用,嵌套函數的定義並調用在局部做用域中完成。它打印的是local。再看這個

    var scope = "global";
    function func(){
        var scope = "local";  // 局部變量
        function nested(){ return scope; }
        return nested;  // 返回這個嵌套函數變量
    }
    console.log(func()());  // 在全局做用域中調用局部的嵌套函數

  前面說過,函數也是變量、對象,也能夠做爲函數返回值,func不直接返回變量,而返回一個內嵌函數,而後在全局做用域調用這個局部內嵌函數,它會返回什麼呢?結果仍爲local。因爲閉包機制,nested函數在定義時就已經決定了,函數體內的scope變量值是local,這是一種綁定關係,不會隨着調用環境的改變而改變,它去對象關聯列表中查找按優先級分的話,老是func函數對應的scope值。

  閉包的功能很強大,看這個例子(例A

    var integer = (function(){
            var i = 0;
            return function(){ return i++; };
        }());
    console.log(integer());  // 0
    console.log(integer());  // 1
    console.log(integer());  // 2

  若是有點C/C++基礎的人,初看這個調用結果,極可能會說這個打印的是0,0,0,反正我是很小白的這樣想,每次調用完,臨時變量i就會被銷燬。可是確實有打印0的狀況,看下這個(例B

    function func(){
        var i = 0;
        function nested(){ return i++; }
        return nested();
    }
    console.log(func());  // 0
    console.log(func());  // 0
    console.log(func());  // 0

  也就是說,這兩個看起來差很少的函數仍是有點差異的。

     在C/C++中,若是不是全局變量或局部靜態變量,只要在局部函數中定義的變量在調用一次完成後就立刻被銷燬,固然除使用malloc、realloc、new等函數開闢的動態空間除外,這種必須得手動釋放,不然容易形成內存泄露。js中是垃圾自動回收機制,某些無用的東西會被自動銷燬,在前兩個例子中,例A顯然沒有被銷燬,而例B中的變量被銷燬了,由於每次調用都是新聲明一個i變量。so why?

  在C中,局部變量被臨時保存在一個棧中,先調用的先入棧,後調用的後入棧,調用完從棧訂彈出,變量內存被銷燬,利用的是棧的後進先出特色。而js依靠的是做用域鏈,這是一個列表或者鏈表,並非棧,沒有所謂的壓入(push)、彈出(pop)操做,若是說定義時就有一個列表的話,每次調用一個函數時,都會建立一個新的、跟它關聯的對象,保存着局部變量,而後把這些對象添加至一個列表中造成做用域鏈,即使調用同一個函數兩次,生成也是兩個列表。

      當一個函數執行完要返回的時候,便把對應對象從列表中刪除,對象中的屬性也會被銷燬,意味着局部函數中的變量將不復存在,在例B中,return nested(),執行完返回一個值,nested函數再無任何做用,被從列表中刪掉了。

  若是一個局部函數定義了嵌套函數,而且有一個外部引用指向這個嵌套函數,就不會被當作垃圾回收。何時會有一個外部引用指向它(內嵌函數)?當它做爲返回值(即返回一個函數變量),或者它做爲某個對象屬性的值存儲起來時。不會被當成垃圾回收,它綁定的對象也不會從對象列表中刪掉,這個綁定對象的屬性和值天然也不會被銷燬,天然能夠進行復用了。因此再次調用其建立一個新的對象列表時,變量的值是在上一次調用的基礎上改變的。

  例A返回的是一個函數變量,意味着有一個外部引用指向着它:Hey!你可能會被調用哦,我不會刪除你的。這樣每次i是累加的。相似例A,若是將函數保存爲一個對象的屬性也不會被刪除,例C

    function counter(){
        var n = 5;
        return {
            count: function(){ return n++; },
            reset: function(){ n = 0; }
        };
    }
    var countA = counter();
    console.log(countA.count());  // 5
    console.log(countA.count());  // 6
    console.log(countA.count());  // 7

  例C返回一個對象,對象的屬性均是函數,也符合上邊的情形,因此n值是累加的。這些狀況下,一般咱們會把相似定義的n稱爲這個外部函數的私有屬性(成員),由於它們運行起來就像是函數的內嵌函數(閉包)共享的東西同樣。前面說過,咱們有時直接將函數稱做閉包,尤爲是同一個函數內部定義的函數

    function func(){
        var funArr = [];
        for(var i = 0; i <= 2; ++i)
            funArr[i] = function(){ return i; };
        return funArr;
    }
    var farr = func();
    console.log(farr[0]());
    console.log(farr[1]());
    console.log(farr[2]());

  能夠說:Here we create three closures, they are all defined in one function, so they share access to the variable i。那麼它們輸出神馬?事實是,它們都打印3。它們共享變量,當func執行完時,i的值爲3,全部的閉包均共享這個值。這個例子說明閉包的一個重要特色:全部閉包在與做用域鏈關聯時是「活動的」,雖然函數必定義完成,做用域鏈就隨着生成,可是全部閉包均不會單獨對做用域內的私有成員(如上例中的i、例C中的n)進行復制一份,不會生成一個靜態快照,而是共享,當這個成員的值改變時,它們返回的值也跟着變化。

  戰戰兢兢寫完,感受仍是要加深理解-_-

相關文章
相關標籤/搜索