第07章 - 函數(shù)表達(dá)式

常規(guī)方式定義函數(shù)

定義函數(shù)有兩種方式,第一種方式為常規(guī)聲明方式,該方式下函數(shù)可以先使用,后聲明,即"函數(shù)聲明提升"特性。

//函數(shù)提升

sayHi();

function sayHi(){
    alert("Hi!");
}

常規(guī)聲明方式方式定義的函數(shù)有 name 屬性,可以獲取函數(shù)的名字

// name 屬性

function functionName(arg0, arg1, arg2) {
//function body
}

alert(functionName.name); //"functionName"

匿名方式定義函數(shù)

第二種方式為匿名函數(shù)方式,即函數(shù)沒(méi)有名字。但是函數(shù)對(duì)象可以賦值給變量,可以作為參數(shù),也可以作為返回值

var functionName = function(arg0, arg1, arg2){
    //function body
};

7.1 遞歸

遞歸就是函數(shù)自己調(diào)用自己,下面求階乘的函數(shù)是個(gè)典型的遞歸函數(shù)

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * factorial(num-1);
    }
}

但是上面這種寫法在 Javascirpt 中可能會(huì)出現(xiàn)錯(cuò)誤,因?yàn)樵?Javascript 中,函數(shù)的名字可能發(fā)生變化

var anotherFactorial = factorial;

factorial = null;//這個(gè)名字不再指向函數(shù)對(duì)象
alert(anotherFactorial(4)); //出錯(cuò)

可以用 arguments.callee 來(lái)解決這個(gè)問(wèn)題,arguments.callee 始終指向當(dāng)前正在執(zhí)行的函數(shù)對(duì)象

function factorial(num){
    if (num <= 1){
        return 1;
    } else {
        return num * arguments.callee(num-1);
    }
}

在有些情況下,arguments.callee 并不存在,可以用命名函數(shù)表達(dá)式來(lái)實(shí)現(xiàn)遞歸

//在函數(shù)定義外部包裹括號(hào),可以獲取函數(shù)對(duì)象
var factorial = (function f(num){
    if (num <= 1){
        return 1;
    } else {
        return num * f(num-1);
    }
});

7.2 閉包

閉包也是一個(gè)函數(shù),但是這個(gè)函數(shù)保存了另外一個(gè)函數(shù)作用域中的變量。通常創(chuàng)建閉包的方式

  • 在一個(gè)函數(shù)中創(chuàng)建另外一個(gè)函數(shù)
  • 內(nèi)部函數(shù)訪問(wèn)了外部函數(shù)中的局部變量
  • 外部函數(shù)執(zhí)行完畢后被銷毀,而內(nèi)部函數(shù)對(duì)象因?yàn)橛懈L(zhǎng)的生命周期而被保留,因而變成了閉包
function createComparisonFunction(propertyName) {

    //定義內(nèi)部函數(shù)
    return function(object1, object2){

        //訪問(wèn)了外部函數(shù)的變量,該函數(shù)將變?yōu)殚]包
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}

執(zhí)行環(huán)境

  • 執(zhí)行環(huán)境是一組數(shù)據(jù),這組數(shù)據(jù)定義了代碼運(yùn)行的邊界,防止出現(xiàn)混亂。
  • 執(zhí)行環(huán)境在函數(shù)執(zhí)行時(shí)候被創(chuàng)建,函數(shù)執(zhí)行完畢后,被銷毀。
  • window 對(duì)象是一個(gè)全局執(zhí)行環(huán)境,不會(huì)被銷毀。

變量對(duì)象

每一個(gè)執(zhí)行環(huán)境都有一個(gè)對(duì)應(yīng)的變量對(duì)象,變量對(duì)象中保存著函數(shù)能訪問(wèn)到的局部變量和函數(shù)的名字。

作用域鏈

每個(gè)執(zhí)行環(huán)境都會(huì)包含一個(gè)作用域鏈,作用域鏈的頂端指向當(dāng)前環(huán)境的變量對(duì)象,下一個(gè)指向外部環(huán)境的變量對(duì)象,最后指向全局環(huán)境的變量對(duì)象。作用域鏈保存在函數(shù)對(duì)象的 [[Scope]] 屬性中

查找變量

函數(shù)查找一個(gè)變量的時(shí)候,先從作用域鏈頂端的變量對(duì)象中查找,找不到則到下一個(gè)變量對(duì)象中查找,一直查找到最后一個(gè)全局環(huán)境的變量對(duì)象為止。

作用域鏈創(chuàng)建的流程

函數(shù)運(yùn)行時(shí),會(huì):

  • 先創(chuàng)建函數(shù)自己的局部執(zhí)行環(huán)境
  • 向執(zhí)行環(huán)境中加入一個(gè)默認(rèn)的作用域鏈,該作用域鏈只包含全局環(huán)境的變量對(duì)象
  • 將函數(shù)自己的變量對(duì)象添加到作用域鏈的頂端

下面的代碼運(yùn)行后,會(huì)產(chǎn)生如圖的數(shù)據(jù)結(jié)構(gòu)

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);
07-01.jpg

閉包為什么能保存外部環(huán)境中的變量

當(dāng)內(nèi)部環(huán)境訪問(wèn)外部環(huán)境的時(shí)候,外部環(huán)境的變量對(duì)象會(huì)被添加到內(nèi)部環(huán)境的作用域鏈中,因此內(nèi)部環(huán)境可以訪問(wèn)外部環(huán)境。

一般情況下,內(nèi)部環(huán)境總是先于外部環(huán)境執(zhí)行完畢,因此先被銷毀

特殊的情況下,外部環(huán)境先被銷毀,而內(nèi)部環(huán)境被長(zhǎng)期保留。這時(shí)候,由于內(nèi)部環(huán)境的作用域鏈中引用了外部環(huán)境的變量對(duì)象,因此外部環(huán)境的變量對(duì)象會(huì)被保留下來(lái),被銷毀的只有外部環(huán)境本身和他的作用域鏈。

07-02.jpg

適度使用閉包并及時(shí)釋放閉包占用的內(nèi)存

  • 因?yàn)殚]包會(huì)阻止外部環(huán)境銷毀其變量對(duì)象,因此閉包會(huì)占用額外內(nèi)存,過(guò)度的閉包會(huì)導(dǎo)致性能下降
  • 可以閉包使用完畢后,及時(shí)將其設(shè)置為 null ,手動(dòng)釋放內(nèi)存

7.2.1 閉包與變量的狀態(tài)

閉包中引用的外部變量,其值可能會(huì)在閉包創(chuàng)建以后繼續(xù)發(fā)生變化,而閉包并不能記住創(chuàng)建時(shí)外部變量的狀態(tài)。

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        //所有閉包引用的都是同一個(gè)外部變量對(duì)象中的同一個(gè) i ,因此只能得到最后的 i 的狀態(tài)
        result[i] = function(){
            return i;
        };
    }
    return result;
}

要想保持閉包創(chuàng)建時(shí)外部變量的狀態(tài),可以將外部變量作為閉包的參數(shù)傳進(jìn)去,這樣閉包中只有對(duì)本地變量使用,不受外部變量變化的影響

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

    for (var i=0; i < 10; i++){
        result[i] = function(num){
            return function(){
                return num;
            };
        }(i);//將狀態(tài)作為參數(shù)穿進(jìn)去
    }

    return result;
}

7.2.2 關(guān)于 this 指針

  • 在全局作用于下,this 指向 window 對(duì)象
  • 在方法中,this 指向方法所在的對(duì)象
  • 在閉包中,this 指向 window 對(duì)象

在下面的例子中,object.getNameFunc()() 將返回一個(gè)閉包。這是因?yàn)椋罱K返回的匿名函數(shù)的外部環(huán)境已經(jīng)不存在了,因此匿名函數(shù)變成了閉包。所以,其中的 this 將指向 window 對(duì)象

var name = "The Window";

var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};

alert(object.getNameFunc()()); //"The Window" (in non-strict mode)

要想在這種情況下讓 this 指向?qū)ο蟊旧恚梢韵扔靡粋€(gè)臨時(shí)變量保存 this ,然后利用閉包的特性引用臨時(shí)變量,而不是直接使用 this

var name = "The Window";

var object = {
    name : "My Object",
    getNameFunc : function(){
        //用一個(gè)臨時(shí)變量保存 this
        var that = this;

        return function(){
            //在內(nèi)部引用臨時(shí)變量
            return that.name;
        };
    }
};

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

7.2.3 閉包的內(nèi)存泄漏:

一般情況下 HTML 對(duì)象的事件屬性會(huì)指向一個(gè)函數(shù)對(duì)象, HTML 對(duì)象被銷毀,函數(shù)對(duì)象也會(huì)被銷毀。

某些特殊情況下,事件處理函數(shù)中,又反過(guò)來(lái)引用了 HTML 對(duì)象,事件處理函數(shù)就變成了一個(gè)閉包。這是一種循環(huán)引用,導(dǎo)致 HTML 對(duì)象將不會(huì)被銷毀

function assignHandler(){
    var element = document.getElementById("someElement");
    //事件處理函數(shù)引用了外部變量,且生存周期比外部環(huán)境長(zhǎng),因此變成了閉包
    //閉包中引用的對(duì)象將不會(huì)被釋放
    element.onclick = function(){
        alert(element.id);
    };
}

解決辦法:可以先將對(duì)象的屬性賦值給外部函數(shù)的一個(gè)局部變量,閉包中引用這個(gè)局部變量而不引用 HTML 對(duì)象。 HTML 對(duì)象使用完成后應(yīng)該賦值 null

function assignHandler(){
    var element = document.getElementById("someElement");
    //將 HTML 對(duì)象復(fù)制給一個(gè)局部變量,閉包中引用這個(gè)局部變量
    var id = element.id;    
    element.onclick = function(){
        alert(id);
    };

    //釋放這個(gè) HTML 對(duì)象 
    element = null;
}

7.3 塊級(jí)作用域

Javascript 中沒(méi)有塊級(jí)作用域的概念。下面的代碼中,臨時(shí)變量 i 超出了作用于仍然存在

function outputNumbers(count){
    for (var i=0; i < count; i++){
        alert(i);
    }

    alert(i); //count
}

可以將使用臨時(shí)變量的代碼放到塊級(jí)作用域中,這樣可以防止命名空間污染,減少內(nèi)存占用

function outputNumbers(count){
    (function () {
        for (var i=0; i < count; i++){
            alert(i);
        }
    })();

    alert(i); //causes an error
}

7.4 私有變量

函數(shù)本身就具有封裝性,在函數(shù)內(nèi)部定義的變量和函數(shù)都是私有的,外部不能訪問(wèn)

///外部看不見(jiàn) num1 num2 等變量
function add(num1, num2){
    var sum = num1 + num2;
    return sum;
}

訪問(wèn)接口

完全私有的變量是沒(méi)有用處的,可以留一個(gè)公共接口供外部訪問(wèn)

function MyObject(){
    //私有變量
    var privateVariable = 10;

    //私有函數(shù)
    function privateFunction(){
        return false;
    }

    //特權(quán)方法,外部可以訪問(wèn)
    this.publicMethod = function (){
        privateVariable++;
        return privateFunction();
    };
}

實(shí)例成員私有模式

這是最基本的模式,每個(gè)實(shí)例都會(huì)創(chuàng)建自己獨(dú)立的成員,不同實(shí)例之間互不影響;通過(guò)特權(quán)函數(shù)可以訪問(wèn)實(shí)例的私有成員。

缺點(diǎn)是:特權(quán)函數(shù)也會(huì)在每個(gè)實(shí)例創(chuàng)建一份,浪費(fèi)資源。

function Person(name){

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

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

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

person.setName("Greg");
alert(person.getName()); //"Greg"

7.4.1 靜態(tài)私有變量

實(shí)現(xiàn)方式:

  • 所有成員,包括私有成員、特權(quán)函數(shù)和構(gòu)造函數(shù)都在一個(gè)自調(diào)用的匿名函數(shù)中定義
  • 通過(guò)匿名函數(shù)的自調(diào)用創(chuàng)建所有成員
  • 其中構(gòu)造函數(shù)不使用 var 關(guān)鍵字,因此可以全局訪問(wèn)
  • 訪問(wèn)接口在原型中定義
(function(){
    //私有成員和私有方法
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    //構(gòu)造函數(shù),沒(méi)有用 var 關(guān)鍵字,所以可以全局調(diào)用
    MyObject = function(){
    };

    //特權(quán)方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };
})();//通過(guò)自調(diào)用創(chuàng)建成員

模式特點(diǎn):

  • 所有成員,包括私有成員、特權(quán)函數(shù)和構(gòu)造函數(shù)只在匿名函數(shù)自調(diào)用的時(shí)候創(chuàng)建一次
  • 私有成員因?yàn)楸粯?gòu)造函數(shù)和特權(quán)函數(shù)引用,因此得以在匿名函數(shù)自調(diào)用完成以后,仍然保留在內(nèi)存中
  • 構(gòu)造函數(shù)可以多次調(diào)用,創(chuàng)建多個(gè)實(shí)例,但是私有成員不會(huì)被再次創(chuàng)建
  • 所有實(shí)例共享私有變量,就好像其他語(yǔ)言中的靜態(tài)變量
(function(){
    //name 本來(lái)是匿名函數(shù)的私有變量,在匿名函數(shù)自調(diào)用的時(shí)候創(chuàng)建

    var name = "";

    //構(gòu)造函數(shù)閉包引用了外部變量,并且生命周期比外部環(huán)境長(zhǎng),因此變成了閉包
    //因此,匿名函數(shù)自調(diào)用完畢后,私有變量不會(huì)被銷毀
    Person = function(value){
        name = value;
    };

    Person.prototype.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"

7.4.2 模塊模式(單例模式)

在匿名函數(shù)中沒(méi)有定義構(gòu)造函數(shù),而是在匿名函數(shù)自調(diào)用以后返回一個(gè)對(duì)象,對(duì)象中包含了公共接口

var singleton = function(){
    //私有成員
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    //返回對(duì)象,
    return {
        publicProperty: true,
        publicMethod : function(){
            privateVariable++;
            return privateFunction();
        }
    };
}();

這種模式下,私有成員同樣只創(chuàng)建一次,通過(guò)公共接口訪問(wèn)

var application = function(){
    //私有成員
    var components = new Array();

    //一些其他的初始化動(dòng)作
    components.push(new BaseComponent());

    //公共接口
    return {
        getComponentCount : function(){
            return components.length;
        },

        registerComponent : function(component){
            if (typeof component == "object"){
                components.push(component);
            }
        }
    };
}();

7.4.2 增強(qiáng)單例模式

和上面的單例模式差不多,只是將返回值由字面量對(duì)象改成普通對(duì)象

var singleton = function(){
    //私有成員
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    //創(chuàng)建一個(gè)對(duì)象
    var object = new CustomType();

    //添加特權(quán)接口
    object.publicProperty = true;
    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    };

    //返回對(duì)象
    return object;
}();

例程

var application = function(){
    //private variables and functions
    var components = new Array();

    //initialization
    components.push(new BaseComponent());

    //create a local copy of application
    var app = new BaseComponent();

    //public interface
    app.getComponentCount = function(){
        return components.length;
    };

    app.registerComponent = function(component){
        if (typeof component == "object"){
            components.push(component);
        }
    };
    
    //return it
    return app;
}();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容