JS學習6(函數表達式)

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

函數聲明

使用函數聲明時,函數聲明會被提升至當前作用域最前面。

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

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

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

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

函數表達式

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

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

不過這樣的特性就可以實現剛才的目的咯~

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

遞歸

就是函數自己調用自己咯

最初級的版本

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

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

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

第二個版本

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

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

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

第三個版本

這時函數表達式就上場了。

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

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

閉包

閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式就是在一個函數中創建另一個函數。就像這樣:

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

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

普通函數的作用域鏈

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

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


這里寫圖片描述

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

閉包的作用域鏈

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

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

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

compareNames = null;
這里寫圖片描述

閉包與變量

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

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

按理說每個函數都應該返回自己的索引數呀。但是對不起,所有匿名函數中保存的createFunctions的活動對象都是一個哦,所以i也是一個哦,最后i變為了10.所以所有匿名函數訪問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是不同的。這樣每次調用時訪問的就是對于每一個元素都不同的num變量了。

關于this對象

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

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

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"

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

內存泄漏

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

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

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

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

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

模仿塊級作用域

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

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

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

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

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

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

私有變量

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

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

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

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對于對象的使用者來說就是透明的。
但是在構造函數中定義特權方法有個問題,那就是你必須使用構造函數模式來達到這個目的,也就是說對于每一個實例都會創造一組新的方法。

靜態私有變量

(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"

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

模塊模式

為單例創建私有變量和特權方法的模式,單例是只有一個實例的對象:

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

模塊模式為單例添加私有變量和特權方法:

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

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

增強的模塊模式

這種模式適合要返回的單例是特定類型的,本質上和普通的模塊模式沒啥區別。

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對象就滿足了必須是某個類型的單例的要求。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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