JS學(xué)習(xí)6(函數(shù)表達式)

定義函數(shù)的方法有兩種:函數(shù)聲明和函數(shù)表達式。

函數(shù)聲明

使用函數(shù)聲明時,函數(shù)聲明會被提升至當(dāng)前作用域最前面。

//這樣也不會報錯
sayHi();
function sayHi(){
    alert("Hi!");
}

但是這個特性也就造成了這樣的使用是不可預(yù)測的:

if(condition){
    function sayHi(){
        alert("Hi!");
    }
} else {
    function sayHi(){
        alert("Yo!");
    } 
}

這段代碼本來的目的是在兩個if的情況下使用不同的函數(shù)定義,但是在函數(shù)聲明被提前的情況下,這顯然是做不到的。所以在JS里永遠(yuǎn)也不要這么做。

函數(shù)表達式

函數(shù)表達式就是將一個匿名函數(shù)賦值給一個變量,所以直到執(zhí)行到這句代碼之前,這個函數(shù)都是不存在的。

sayHi(); // 報錯函數(shù)不存在
var sayHi = function(){
    alert("Hi!");
};

不過這樣的特性就可以實現(xiàn)剛才的目的咯~

var sayHi;
if(condition){
    sayHi = function(){
        alert("Hi!");
    };
} else {
    sayHi = function(){
        alert("Yo!");
    };
}

遞歸

就是函數(shù)自己調(diào)用自己咯

最初級的版本

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

這個看起來并沒有什么問題呀,在函數(shù)定義里調(diào)用了自己。但是由于有函數(shù)表達式的存在,這樣就會出錯了:

//將anotherFactorial指向函數(shù)
var anotherFactorial = factorial;
//將factorial指向空
factorial = null;
//再調(diào)用這個函數(shù)時,里面的factorial(num-1)就無法執(zhí)行了
alert(anotherFactorial(4)); 

第二個版本

使用arguments.callee這個指向正在執(zhí)行函數(shù)的指針可以解決這個問題。

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

這樣就解決了函數(shù)名在遞歸函數(shù)里被寫死的問題。但是在嚴(yán)格模式下訪問不到這個指針哦,所以。。。。還得想個辦法。

第三個版本

這時函數(shù)表達式就上場了。

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

這樣寫即便把函數(shù)再賦值給別的變量f()還是可以保留。其實這個和第一種方法的本質(zhì)是一樣的,而且有個大問題就是這個函數(shù)表達式要放在調(diào)用前面。

閉包

閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見方式就是在一個函數(shù)中創(chuàng)建另一個函數(shù)。就像這樣:

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" });

在內(nèi)部定義的匿名函數(shù)中,訪問了外部函數(shù)中的變量propertyName。即使這個匿名函數(shù)被返回賦值給另一個變量,在其他地方通過這個變量調(diào)用了這個匿名函數(shù),它仍然可以訪問propertyName這個變量。這是因為這個匿名函數(shù)的作用域鏈里包含著createComparisonFunction()的作用域。

普通函數(shù)的作用域鏈

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

在這里我們先定義了compare()函數(shù),然后又在全局作用域中調(diào)用了它。
當(dāng)某個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行環(huán)境及相應(yīng)的作用域鏈。每一個執(zhí)行環(huán)境都有一個表示變量的對象(變量對象),全局的變量對象始終存在,而像函數(shù)這樣的局部環(huán)境的變量對象則只存在于函數(shù)執(zhí)行的過程中。
在創(chuàng)建compare()時,會預(yù)先創(chuàng)建一個包含全局變量對象的的作用域鏈,這個作用域鏈被保存在內(nèi)部的[[Scope]]屬性中。
當(dāng)調(diào)用compare()時,函數(shù)的執(zhí)行環(huán)境就被創(chuàng)建了,并通過復(fù)制函數(shù)[[Scope]]的對象構(gòu)建起該執(zhí)行環(huán)境的作用域鏈。接下來會創(chuàng)建這個函數(shù)的活動對象,這個對象包括arguments和其他命名參數(shù),活動對象在此作為這個局部執(zhí)行環(huán)境的變量對象被推入作用域鏈的最前端。作用域鏈本質(zhì)上就是一個指向變量對象的指針列表。


這里寫圖片描述

在函數(shù)中訪問一個變量時,就會在作用域鏈中搜索具有相應(yīng)名字的變量。對于一般的函數(shù),在執(zhí)行完畢后活動對象(局部變量對象)就會被銷毀。內(nèi)存中僅保留全局執(zhí)行環(huán)境的變量對象。

閉包的作用域鏈

在一個函數(shù)內(nèi)部定義的函數(shù)會將外部函數(shù)的活動對象添加到它的作用域鏈中。
以上面的例子來說

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

當(dāng)匿名函數(shù)從createComparisonFunction()返回到compare變量中時同樣要創(chuàng)建一個作用域鏈,這個作用域鏈里不僅包含全局執(zhí)行環(huán)境的變量對象,還包含createComparisonFunction()的活動對象。
這就意味著匿名函數(shù)可以訪問createComparisonFunction()中定義的所有變量。且在createComparisonFunction()執(zhí)行完之后,其執(zhí)行環(huán)境的作用域鏈被銷毀了,但是其活動對象并不像往常一樣會被銷毀,而是還留存在內(nèi)存中。因為這時匿名函數(shù)的作用域鏈仍在引用這個對象。所以直到匿名函數(shù)被銷毀,createComparisonFunction()的活動對象才會被消滅。

compareNames = null;
這里寫圖片描述

閉包與變量

閉包所保存的是外層函數(shù)的整個變量對象,也就是說,閉包只能外面的變量的最終一個值。

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

按理說每個函數(shù)都應(yīng)該返回自己的索引數(shù)呀。但是對不起,所有匿名函數(shù)中保存的createFunctions的活動對象都是一個哦,所以i也是一個哦,最后i變?yōu)榱?0.所以所有匿名函數(shù)訪問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;
}
var a = createFunctions();
alert(a[1]());   //1

由于i到num是值傳遞的,所以對于每一個result[i],num是不同的。這樣每次調(diào)用時訪問的就是對于每一個元素都不同的num變量了。

關(guān)于this對象

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

每個函數(shù)在被調(diào)用時都會自動取得兩個特殊變量,this和arguments。函數(shù)在搜索這兩個變量的時候只搜索到自己的活動對象為止,并不在作用域鏈中搜索(其實這很好想,就相當(dāng)于自己的執(zhí)行環(huán)境里有這兩個變量,為啥還要往上找。)匿名函數(shù)最后是在全局變量中執(zhí)行的,所以this指向全局環(huán)境。

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

object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window"

有時即使是細(xì)微的變化也會引起this的值的變化。這里后兩個例子不太明白。

內(nèi)存泄漏

這個主要是因為IE9之前對于JS對象和BOM/DOM對象采用不同的垃圾回收機制造成的。對DOM元素使用的引用計數(shù)方法無法應(yīng)對循環(huán)引用的情況。

function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function(){
        alert(element.id);
    };
}

在這里element對匿名函數(shù)有引用,匿名函數(shù)引用著assignHandler()的活動對象,其中就包含著element,啊哦。
所以。。。。。拋棄IE吧!
好吧。。。。可以這樣改。。。

function assignHandler(){
    var element = document.getElementById("someElement"); 
    var id = element.id;
    element.onclick = function(){
        alert(id);
    };
    element = null;
}

僅僅把閉包中的element移出去是木有用噠,閉包包含著外面函數(shù)的活動對象哦,element的引用還在,所以要手工減掉element對DOM的引用,對匿名函數(shù)的引用也就沒了。

模仿塊級作用域

對于多次聲明同一個變量,JS不會阻止你這樣做,而且如果你在多次的聲明中執(zhí)行了變量初始化,JS會照做不誤。這就帶來了很大的問題,你自己的代碼可能比較了解,但是當(dāng)你使用了框架或別人的JS時,天知道你們的變量名有沒有重復(fù)的!
所以,模仿塊級作用域的需求就來了。
看看是怎么演變來的

var someFunction = function(){
    //塊級作用域      
};
someFunction();

我們是可以用時機的值替換變量名的

function(){ 
    //塊級作用域     
}(); //報錯   

這里會報錯是因為JS將function視作一個函數(shù)聲明的開始,函數(shù)聲明后面不能跟圓括號。那么我們將它轉(zhuǎn)換為函數(shù)表達式吧

(function(){
    //塊級作用域    
})();

私有變量

任何在函數(shù)中定義的變量就是私有變量,因為外部訪問不到?,F(xiàn)在我們有了閉包,閉包可以被返回到作用域外面,而通過閉包又可以訪問作用域里的變量和方法。這樣我們就可以來模仿私有變量和公有方法了。

function MyObject(){
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    this.publicMethod = function (){
        privateVariable++;
        return privateFunction();
    };
}

privateVariable和privateFunction()只有通過publicMethod方法才能訪問。
使用這樣的方法,我們可以隱藏那些不能被修改的數(shù)據(jù)。

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"

這里如果我們只提供get方法,name就不會被改變。都不提供那name對于對象的使用者來說就是透明的。
但是在構(gòu)造函數(shù)中定義特權(quán)方法有個問題,那就是你必須使用構(gòu)造函數(shù)模式來達到這個目的,也就是說對于每一個實例都會創(chuàng)造一組新的方法。

靜態(tài)私有變量

(function(){
    var name = "";
    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"

靜態(tài)私有變量創(chuàng)建了一個私有作用域,其中Person這個變量定義時沒有通過var聲明,這樣它就是個全局變量。于是在它原型里的方法在私有作用域外也可以訪問到。但這里name屬性是所有實例共享的。

模塊模式

為單例創(chuàng)建私有變量和特權(quán)方法的模式,單例是只有一個實例的對象:

var singleton = {
    name : value,
    method : function () { 
    } 
};

模塊模式為單例添加私有變量和特權(quán)方法:

var application = function(){
    var components = new Array();
    components.push(new BaseComponent());
    return {
        getComponentCount : function(){
            return components.length;
        },
        registerComponent : function(component){
            if (typeof component == "object"){
                components.push(component);
            }
        } 
    };
}();

在這里,最后返回的單例對象是在匿名函數(shù)里創(chuàng)建的,所以這里面的方法可以訪問到匿名函數(shù)里的私有變量。外界只能通過著兩個方法有限的修改components,只能獲取長度和添加。但是這里的單例是Object對象。

增強的模塊模式

這種模式適合要返回的單例是特定類型的,本質(zhì)上和普通的模塊模式?jīng)]啥區(qū)別。

var application = function(){
    var components = new Array();
    components.push(new BaseComponent());
    
    var app = new BaseComponent();
    app.getComponentCount = function(){
        return components.length;
    };
    app.registerComponent = function(component){
        if (typeof component == "object"){
            components.push(component);
        }
    };     
    return app;
}();

這樣app對象就滿足了必須是某個類型的單例的要求。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 定義函數(shù)的方式有兩種:函數(shù)聲明和函數(shù)表達式。 函數(shù)聲明的一個重要特征就是函數(shù)聲明提升,意思是在執(zhí)行代碼前會先讀取函...
    oWSQo閱讀 752評論 0 0
  • 本章內(nèi)容 函數(shù)表達式的特征 使用函數(shù)實現(xiàn)遞歸 使用閉包定義私有變量 定義函數(shù)的方式有兩種:一種是函數(shù)聲明,另一種就...
    悶油瓶小張閱讀 450評論 0 0
  • 最近學(xué)這塊知識學(xué)得有些吃力。還有很多遺漏的地方,只能以后多看些書來彌補了。 第7章 函數(shù)表達式 函數(shù)定義的兩種方式...
    丨ouo丨閱讀 481評論 0 1
  • 一早醒來,發(fā)現(xiàn)周圍不再是熟悉的場景。首先看到的,也不再是天花板,而是太陽初升粉紅橙色的朝霞滿天,有多久沒有見到這么...
    繁花塢閱讀 6,754評論 10 16
  • 詞:董書利你是一本書未輕易去翻那不加修飾的封面足以讓我思緒萬千 你寧愿是迷我無力承受失去也不想歷史重演來祭奠自己的...
    星巢文化閱讀 337評論 4 4

友情鏈接更多精彩內(nèi)容