JavaScript高級程序設計之函數表達式

定義函數的方式有兩種:一種是函數聲明,另一種就是函數表達式。

//函數聲明
function functionName(arg0, arg1, arg2) {
    //函數體
}

//函數表達式
var functionName = function(arg0, arg1, arg2) {
    //函數體
}

函數聲明的一個重要特征就是函數聲明提升(function declaration hoisting),意思是在執行代碼前會先讀取函數聲明。



<script type=”text/javascript”>

sayHi();
function sayHi(){
alert(“Hi!”);
}

</script>

這個例子不會報錯,因為代碼在執行前會先讀取函數聲明。理解函數提升的關鍵就是區別函數聲明和函數表達式之間的區別。
函數表達式的方式有幾種表達形式,其中很常見的一種形式就是匿名函數的形式:

var functionName = function(arg0,arg1,arg2){
    //函數體
}

這種情況下創建的函數叫做匿名函數,因為function關鍵字後面沒有標識符。匿名函數的name屬性是空字符串。

閉包

不少開發人員總是搞不清匿名函數和閉包這兩個概念。閉包是指有權訪問另一個函數作用域中的變量的函數。而匿名函數是指沒有函數名稱的函數。創建閉包的常見方式,就是在一個函數內部創建另一個函數。

function createComparisonFunction(propertyName) {

    return function (object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if(value1<value2) return="" else="" pre="">

無論什麼時候在函數中訪問一個變量時,都會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢後,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同。 在另一個函數內部定義的函數將會包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函createComparisonFunction()的活動對象。下圖展示瞭當下列代碼執行時,包含函數與內部匿名函數的作用域鏈。

var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

**當createComparisonFunction()函數返回後,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中;知道匿名函數被銷毀後,createComparisonFunction()的活動對象才會被銷毀**

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

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

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

閉包的特點:由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包會導致內存占用過多。 **作用域鏈的這種配置機制引出瞭一個值得註意的副作用,即閉包隻能取得包含函數中任何變量的最後一個值。閉包所保存的是整個變量對象,而不是某個特殊的值**

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

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

    return result;
}

每個函數的返回值都是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;
}

這裡匿名函數有一個參數num,也就是最終要返回的參數,在調用每個函數時,我們傳入瞭變量i。由於參數是傳遞的,會將變量i的當前值復制給參數num,而在這個匿名函數內部,又創建並返回一個訪問num的閉包,因此,result數組中的每個函數都有自己num變量的一個副本。 關於this對象 因為**匿名函數**(非匿名函數則this指針指向當前執行環境)的執行環境具有全局性,因此其this指針通常指向window。如果想訪問作用域中的arguments對象,必須將對象的引用保存到另一個閉包能夠訪問的變量中。

var name = "The Window";

var object = {
    name: "My Object",

    getNameFunc: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};

alert(object.getNameFunc()());  //"My Object"

私有作用域(塊級作用域)

JavaScript從來不會告訴你是否多次聲明瞭同一個變量;遇到這種情況,它隻會對後續的聲明視而不見(不過,他會執行後續聲明中的變量初始化)。匿名函數可以用來模仿塊級作用域並避免這個問題。 用作塊級作用域(通常稱為私用作用域)的匿名函數的語法如下所示。

//立即調用函數表達式
(function()) {
    //這裡是塊級作用域
})();

以上代碼定義並立即調用瞭一個匿名函數。將函數聲明包含在一對圓括號中,表示它實際是一個函數表達式。而緊隨其後的圓括號會立即調用這個函數。需要註意的是函數聲明後不能跟圓括號而函數表達式是可以的:

function(){
    //這裡是塊級作用域
}();//出錯

在匿名函數中定義的任何變量,都會在執行結束時銷毀。無論在什麼地方,隻要臨時需要一些變量,就可以使用私有作用域,例如:

function outputNumbers(count) {
    (function() {
        for(var i=0; i<count; pre="">

這種技術經常在全局作用域中被用在函數外部,從而限制向全局作用域中添加過多的變量和函數。一般來說,我們都應該盡量少向全局作用域中添加變量和函數。在一個有很多開發人員共同參與的大型應用程序中,過多的全局變量和函數很容易導致命名沖突。而通過創建私有作用域,每個開發人員既可以使用自己的變量,又不必擔心搞亂全局作用域。

私有變量

(使用構造函數) 嚴格來講,JavaScript中沒有私有成員的概念;所有對象屬性都是公有的。不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認為是私有變量,因為不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其它函數。 如果在函數內部創建閉包,那麼閉包通過自己的作用域鏈也可以訪問這些變量。而利用這一點,就可以創建用於訪問私有變量的公有方法。 我們把有權訪問私有變量和私有函數的公有方法稱為特權方法(privileged method)。使用私有和特權成員,可以隱藏那些不應該被直接修改的數據:

function MyObject() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //特權方法
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    };
}
function Person(name) {

    this.getName = function() {
        return name;
    };

    this.setName = function (value) {
        name = value;
    };
}

var person = new Person("Nihcholas");
alert(person.getName());    //"Nicholas"
person.setName("Greg");
alert(person.getName());    //"Greg"

在Perso構造函數的外部,沒有任何方法訪問name。由於這兩個方法是在構造函數內部定義的,它們作為閉包能夠通過作用域鏈訪問name。構造函數定義特權方法也有一個缺點,就是必須使用構造函數模式來達到這個目的。而構造函數模式的缺點是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法就可以避免這個問題。

靜態私有變量

(使用匿名函數嵌套) 通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法,其基本模式如下:

(function() {

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //構造函數
    MyObject = function() {
    };

    //公有/特權方法
    MyObject.prototype.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    };

})();

這個模式創建瞭一個私有作用域,並在其中封裝瞭一個構造函數及相應的方法。在私有作用域中,首先定義瞭私有變量和私有函數,然後又定義瞭構造函數及其公有方法。公有方法是在原型上定義的,這一點體現瞭典型的原型模式。**需要註意的是,在定義構造函數時並沒有使用函數聲明,而是使用函數表達式,函數聲明隻能創建局部函數,處於同樣原因,沒有在聲明MyObject時使用var關鍵字,記住:初始化未經聲明的變量,總是會創建一個全局變量,MyObject就成為瞭一個全局變量,能夠在私有作用域之外被訪問**。 這個模式與在構造函數中定義特權方法的主要區別,就在於**私有變量和函數是由實例共享的**。由於特權方法是在原型上定義的,因此所有實例都使用同一個函數。而這個特權方法,作為一個閉包,總是保存著對包含作用域的引用。

(function() {

    var name = "";

    Person = function(value) {
        name = value;
    };

    Person.prototye.getName = function() {
        return name;
    };

    Person.prototype.setName = function(value) {
        name = value;
    };
})();

var person1 = new Person("Nicholas");
alert(person1.getName());   //"Nicholas"
person1.setName("Greg");
alert(person1.getName());   //"Greg"

var person2 = new Person("Michael");
alert(person1.getName());   //"Michael"
alert(person2.getName());   //"Michael"

在一個實例上調用setname()會影響所有實例。**調用setName()或創建一個Person實例都會賦予name屬性一個新值。結果就是所有實例都會返回相同的值。**以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。到底是使用實例變量,還是靜態私有變量,最終還是要視你的具體需求而定。

小結

在JavaScript編程中,函數表達式是一種非常有用的技術。使用函數表達式可以無須對函數命名,從而實現動態編程。匿名函數,也稱為拉姆達函數,是一種使用JavaScript函數的強大方式。以下總結瞭函數表達式的特點。

函數表達式不同於函數聲明。函數聲明要求有名字,但函數表達式不需要。沒有名字的函數表達式也叫作匿名函數; 在無法確定如何引用函數的情況下,遞歸函數就會變得比較復雜; 遞歸函數應該始終使用argument.callee來遞歸調用自身,不要使用函數名——函數名可能會發生變化。 當函數內部定義瞭其它函數時,就創建瞭閉包。閉包有權訪問包含函數內部的所有變量,原理如下。

在後臺執行環境中,閉包的作用域鏈包含著它自己的作用域、包含函數的作用域和全局作用域; 通常,函數的作用域及其所有變量都會在函數執行結束後被銷毀; 但是,當函數返回瞭一個閉包時,這個函數的作用域將會一直在內存中保存到閉包不存在為止。 使用閉包可以在JavaScript中模仿塊級作用域(JavaScript本身沒有塊級作用域的概念),要點如下:

創建並立即調用一個函數,這樣既可以執行其中的代碼,又不會在內存中留下對該函數的引用。 結果就是函數內部的所有變量都會被立即銷毀——除非將某些變量賦值給瞭包含作用域(即外部作用域)中的變量。 閉包還可以用於在對象中創建私有變量,相關概念和要點如下:

及時JavaScript中沒有正式的私有對象屬性的概念,但可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變量; 有權訪問私有變量的公有方法叫做特權方法; 可以使用構造函數模式、原型模式來實現自定義類型的特權方法,也可以使用塊級模式、增強的模塊模式來實現單例的特權方法。 JavaScript中的函數表達式和閉包都是極其有用的特性,利用它們可以實現很多功能。不過,因為創建閉包必須維護額外的作用域,所以過度使用它們可能會占用大量內存。

發佈留言