JavaScript高級程序設計--函數小記

執行環境和做用域鏈

  每一個函數都有本身的執行環境。當執行流進入一個函數時,函數 的環境就會被推入一個環境棧中。而在函數執行以後,棧將其環境彈出,把控制權返回給以前的執行環境。前端

  當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈。做用域鏈的用途,是保證對執行環境有權訪問的全部變量和函數的有序訪問。做用域鏈的前端,始終都是 當前執行的代碼所在環境的變量對象。若是這個環境是函數,則將其活動對象做爲變量對象。活動對象在最開始時只包含一個變量,即arguments對象(這個對象在全局環境中是不存在的)。做用域量中的下一個變量對象來自包含(外部)環境,而再下一個變量對象則來自下一個包含環境。這樣,一直延續到全局執行環境;全局執行環境的變量對象始終都是做用域鏈中的最後一個環境。編程

var color='bule';
    function changeColor(){
        var anotherColor='red';
        function swapColors(){
            var tempColor=anotherColor;
            anotherColor=color;
            color=tempColor;
            //這裏能夠訪問color、anotherColor和tempColor
        }
        //這裏能夠訪問color、anotherColor,但不能訪問tempColor
        swapColors();
    }
    //這裏只能訪問color
    changeColor();

  內部環境能夠經過做用鏈訪問全部的外部環境,但外部環境不能訪問內部環境中的任何變量和 函數。這些環境的聯繫是線性、有序的。每一個環境均可以向上搜素做用域鏈,以查詢變量和函數名;但任何環境都不能經過向下搜素做用域鏈而進入另外一個執行環境。對於上例中的swapColors()而言,其做用域鏈中包含3個對象:swapColors()的變量對象、changColor()的變量對象和全局變量對象。swapColors()的局部環境開始時會先在本身的變量對象中搜索變量和函數名,若是搜索不到則再搜索上一級做用域鏈。changeColor()的做用域鏈中只包含兩個對象:它本身的變量對象和全局變量對象。這也就是說,它不能訪問swapColors()的環境。數組

  注意:函數參數也被看成變量來對待,所以其訪問規則與執行環境中的其餘變量相同。安全

函數閉包

  閉包是指有權訪問另外一個函數做用域中的變量的函數。建立閉包的常見方式,就是在一個函數內部建立另外一個函數。閉包

  理解閉包就要先理解做用域鏈。當某個函數調用時,會建立一個執行環境及相應的做用域鏈。而後,使用arguments和其餘命名參數的值來初始化函數的活動對象。但在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,......直至做爲做用域鏈重點的全局執行環境。
  在函數執行過程當中,爲讀取哈寫入變量的值,就須要在做用域鏈中查找變量。函數

function compare(value1,value2){
    if(value1<value2){
        return -1;
    }else if(value1>value2){
        return 1;
    }else{
        return 0;
    }
}
var result=compare(5,10);

  以上代碼先定義了compare()函數,而後又在全局做用域中調用了它。當調用compar()時,會建立一個包含arguments、value1和value2的活動對象。全局執行環境的變量對象(包含result和compare)在compare()執行環境的做用域鏈中則處於第二位。如圖展現了包含上述關係的compare()函數執行時的做用域鏈。指針

  後臺的每執行環境都有一個表示變量的對象——變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。在建立compare()函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的[[Scope]]屬性中。當調用compare()函數時,會爲函數建立一個執行環境,而後經過複製函數的[[Scope]]屬性中的對象構建起執行環境的做用域鏈。此後,又有一個活動對象(在此做爲變量對象使用)被建立並被推入執行環境做用域鏈前端。對於這個例子中compare()函數的執行環境而言,其做用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,做用域鏈本質上是一個指向變量兌現的指針列表,它只引用但不實際包含變量對象。code

  不管何時在函數中訪問一個變量時,就會從做用域鏈中搜索具備相應名字的變量。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的變量對象)。可是,閉包的狀況又有所不一樣。對象

  在另外一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的做用域鏈中。所以,在createComparisonFunction()函數內部定義的匿名函數的做用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。下圖展現了當下列代碼執行時,包含函數與內部匿名函數的做用域鏈。blog

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 compare=createComparisonFunction("name");
    var result=compare({name:"Nicholas"},{name:"Greg"});

  在匿名函數從createComparisonFunction()中被返回後,它的做用域鏈唄初始化爲包含createComparisonFunction()函數的活動對象和全局變量對象。這樣,匿名函數就能夠訪問在createComparisonFunction()中定義的全部變量。更爲重要的是,createComparisonFunction()函數在執行 完畢後,其活動對象也不會被銷燬,所以匿名函數的做用域鏈仍然在引用這個活動對象。換句話說,當createComparisonFunction()函數返回後,其執行環境的做用域鏈會被銷燬,但它的活動對象仍然會留在內存中;直到匿名函數唄銷燬後,createComparisonFunction()的活動對象纔會被銷燬,例如:

//建立函數
var compareNames=createComparisonFunction("name");

//調用函數
var result=compareNames({name:"nicholas"},{name:"greg"});

//解除對匿名函數的引用(以便釋放內存)
compareNames=null;

  首先,建立的比較函數被保存在變量compareNames中。而經過將compareNames設置爲等於null解除該函數的引用,就等於通知垃圾回收例程將其清除。隨着匿名函數的做用域鏈被銷燬,其餘做用域(除了全局做用域)也均可以安全地銷燬了。圖展現了調用compareNames()的過程當中產生的做用域鏈之間的關係。

  注意:因爲閉包會攜帶包含它的函數的做用域,所以會比其餘函數佔用更多的內存。過分使用閉包可能會致使內存佔用過多,因此要慎重使用閉包。

閉包與變量

  做用域鏈的這樣配置機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一個值。別忘了閉包所保存的是整個變量對象,而不是某個 特殊的變量。下面這個例子能夠清晰地說明這個問題。

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

    return result;
}

  這個函數會返回一個函數數組。表面上看,彷佛每一個函數都應該返回本身的索引值,即位置0的函數返回0,位置1的函數返回1,以此類推。但實際上,每一個函數都返回10。由於每一個函數的做用域鏈中都保存着 createFunctions()函數的活動對象,因此它們引用的都是同一個變量i。當createFunctions()函數返回後,變量i的值都是10,此時每一個函數都引用着保存變量i的同一個變量對象,因此在每一個函數內部i的 值都是10。可是,咱們能夠經過建立另外一個匿名函數強制讓閉包的行爲符合預期,以下所示。

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

    return result;
}

  在重寫了前面的createFunctions()函數後,每一個函數就會返回各自不一樣的索引值了。在這個版本中,我沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結構哦賦值給數組。這裏的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每二個匿名函數時,咱們傳入了變量i。因爲函數參數是按值傳遞的,因此就會將變量i的當前值複製給參數num。而在這個匿名函數內部,又建立並返回了一個num的閉包。這樣一來,result數組中的每一個函數都有本身num變量的一個副本,所以就能夠返回各自不一樣的數值了。

模仿塊級做用域

  在ES5中沒有塊級做用域的概念。這意味着在塊語句中定義的變量,實際是上在包含函數中而非語句中建立。

  JavaScript歷來不會告訴你是否屢次聲明瞭同一個變量;遇到這種狀況,它只會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。匿名函數能夠用來模仿塊級做用域並避免這個問題。

  用塊級做用域(一般稱爲私有做用域)的匿名函數的語法以下所示。

(function(){
    //這裏是塊級做用域
})();

  以上代碼定義並當即調用了一個匿名函數。將函數聲明包含在一對圓括號中,表示它其實是一個函數表達式。而緊隨其後的另外一對圓括號會當即調用這個函數。

  不管在什麼地方,只要臨時須要一些變量,就可使用私有做用域。

私有變量

  嚴格的說,JavaScript沒有私有成員的概念;全部屬性都是共有的。不過有一個私有變量的概念。任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定的其餘函數。

小結

  在JavaScript編程中,函數表達式是一種很是有用的技術。使用函數表達式能夠無須對函數命名,從而實現動態編程。匿名函數,也稱爲拉姆達函數,是一種使用JavaScript函數的強大方式。如下總結了函數表達式的特色。

  • 函數表達式不一樣於函數聲明。函數聲明要求有名字,但函數表達式不須要。沒有名字的函數表達式也叫作匿名函數。

  • 在沒法肯定如何引用函數的狀況下,遞歸函數就會變得比較複雜;
  • 遞歸函數應該始終使用arguments.callee來遞歸地調用自身,不要使用函數名-函數名可能會發生變化。

當在函數內部定義了其餘函數時,就建立了閉包。閉包有權訪問包含函數內部的全部變量,原理以下。

  • 在後臺執行環境中,閉包的做用域鏈包含着它本身的做用域、包含函數的做用域和全局做用域。
  • 一般,函數的做用域及其全部變量都會在函數執行結束後唄銷燬。
  • 可是,當函數返回了一個閉包時,這個函數的做用域將會一直在內存中保存到閉包不存在爲止。

使用閉包能夠在JavaScript中模仿塊級做用域(JavaScript自己沒有塊級做用域的概念),要點以下。

  • 建立並當即調用一個函數,這樣既能夠執行其中的代碼,又不會在內存中留下對該函數的引用。
  • 結果就是函數內部的全部變量都會被當即銷燬-除非將某些變量賦值給了包含做用域(即外部做用域)中的變量。

閉包還能夠用於在對象中建立私有變量,相關概念和要點以下。

  • 即便JavaScript中沒有正式的私有對象屬性的概念,但可使用閉包來實現公有方法,而經過公有方法能夠訪問在包含做用域中定義的變量。
  • 有權訪問私有變量的公有方法叫作特權方法。
  • 可使用構造函數模式、原型模式來實現自定義類型的特權方法,也可使用模塊模式、加強的模塊模式來實現單例的特權方法。

JavaScript中的函數表達式和閉包都是極其有用的特性,利用它們能夠實現不少功能。不過,由於建立閉包必須維護額外的做用域,因此過分使用它們能夠會佔用大量內存。

相關文章
相關標籤/搜索