對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精華,簡單說這里有用的。函數的定義常見的是

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

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

  因為函數也是對象、變量,所以可以賦給一個變量(更准確說是賦給左值),在這個變量后面使用()調用運算符就可以調用這個函數了,如f1()。還有一種方式:定義即調用

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

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

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

    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)進行復制一份,不會生成一個靜態快照,而是共享,當這個成員的值改變時,它們返回的值也跟着變化。

  戰戰兢兢寫完,感覺還是要加深理解-_-

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM