8、函數表達式1(《JS高級》筆記)

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

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

說明:Firefox、Safari、ChromeOpera都給函數定義了一個非標準的name屬性,通過這個屬性可以訪問到給定函數指定的名字。

console.log(functionName.name);//"functionName"

關于函數聲明,它的一個重要特征就是函數聲明提升,意思是在執行代碼之前會先讀取函數聲明。這意味著可以版函數聲明放在調用它的語句后面。

sayHi();
function sayHi(){
    console.log("Hi");
}

第二種創建函數的方式是使用函數表達式。函數表達式有幾種不同的語法形式,下面是最常見的一種。

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

說明:這種情況下創建的函數叫做匿名函數,因為function關鍵字后面沒有標識符。匿名函數的name屬性是空字符串。函數表達式與其他表達式一樣,在使用前必須先賦值。

理解函數提升的關鍵,就是理解函數聲明與函數表達式之間的區別,如:

var condition = true;

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

sayHi();

說明:表面上看以上代碼表示在conditiontrue時,使用一個sayHi()的定義;否則使用另一個定義。實際上,這是一個無效語法,JS引擎會嘗試修正錯誤,將其轉換為合理的狀態。但是各瀏覽器修正錯誤的做法并不一致,大多數瀏覽器返回第二個聲明,忽略condition。因為這種使用方式很危險,不應該出現。不過,如果使用函數表達式就沒有什么問題了。

var condition = true;
var sayHi;
//never do this!
if(condition){
    sayHi = function(){
        alert("Hi!");
    };
} else {
    sayHi = function(){
        alert("Yo!");
    };
}

sayHi();

一、遞歸

遞歸函數是在一個函數通過名字調用自身的情況下構成,如下所示:

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

這是一個經典的遞歸階乘函數。雖然表面上看沒什么問題,但是下面的代碼卻可能導致其出錯。

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4));  //出錯!

說明:雖然我們先將factorial()函數賦給了anotherFactorial,但是此后將factorial()置為空,當調用anotherFactorial()時,其內部需要調用factorial(),而此時卻為null,于是出錯。這種情況下,使用arguments.callee可以解決問題。

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

說明:arguments.callee是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用。但是在嚴格模式下,不能這樣使用,可以使用命名函數表達式來達到相同的結果:

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

以上代碼創建了一個名為f的命名函數,然后將其賦值給factorical,即便把函數賦值給了另一個變量,函數名f仍然有效。

二、閉包

閉包是指有權訪問另一個函數作用域中的變量的函數。有關如何創建作用域鏈以及作用域鏈有什么用處的細節,對徹底理解閉包至關重要。當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。然后,使用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()時,會創建一個包含arguments、value1value2的活動對象。全局執行環境對象(包含resultcompare)在compa()執行環境的作用域鏈中則處于第二位。如圖所示:

1

說明:

  • 其實很好理解,就是在調用compare()函數時,會創建一個作用域鏈,這個作用域鏈像一個指針數組,首個元素都是指向其本身的活動對象(就是其本身函數體中定義的一些變量),其次就是包含函數(即本函數的上一層或叫包含本函數的函數或環境,此處沒有,則上一層就是全局變量對象)的活動對象,此處為全局變量對象。

  • 在創建compare()函數時,會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內部的[[Scope]]屬性中。當調用compare()函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]屬性中的對象構建起執行環境中的作用域鏈。此后,又有一個活動對象(在此作為變量對象使用)被創建并被推入執行環境作用域鏈的前端。對于這個例子中compare()函數的執行環境而言,其作用域鏈中包含兩個變量對象:本地活動對象和全局變量對象。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象

  • 無論什么時候在函數中訪問一個變量,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢之后,局部活動對象就會被銷毀,內存中僅保存全局作用域。但是閉包的情況又有所不同。

在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。如:

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

如上,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上會包含外部函數createComparisonFunction()的活動對象,如圖所示:

2

代碼如下所示:

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

說明:在匿名函數從createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含createComparisonFunction()函數的活動對象和全局變量(從匿名函數的作用域鏈的后面兩個位置可以看到)。這樣,匿名函數就可以訪問在createComparisonFunction()函數中定義的所有變量。更為重要的是,createComparisonFunction()函數在執行完畢后,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象,而直到匿名函數被銷毀后,createComparisonFunction()的活動對象才會被銷毀。如:

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

最后一行是將匿名函數銷毀。

2.1 閉包與變量

作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值。下面這個例子可以清晰地說明這個問題:

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,于是每個函數都返回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;
}
//換一種寫法
function createFunctions(){
    var result = new Array();
    function indexFunc(num){
        return function(){
            return num;
        }
    }
    for (var i=0; i < 10; i++){
        result[i] = indexFunc(i);
    }
    return result;
}

說明:以上代碼中,沒有直接把閉包賦值給數組,而是定義了一個匿名函數,并將立即執行該匿名函數的結果賦給數組。在調用每個匿名函數時,我們傳入了變量i,由于函數參數是按值傳遞的,所以就會將變量i的當前值復制給參數num,而在這個匿名函數內部,又創建并返回了一個訪問num的閉包,這樣,result數組中的每個函數都有自己的num變量副本,因此就可以返回各自的索引了。

2.2 關于this變量

我們知道,this對象是在運行時基于函數的執行環境綁定的:在全局函數中,this等于window,而當函數被作為某個對象的方法調用時,this等于那個對象。不過,匿名函數的執行環境具有全局性,因此this對象通常指向window(當然,在通過call()applay()改變函數執行環境的情況下,this就會指向其他對象)。但有時候由于編寫閉包的方式不同,這一點可能不會那么明顯,如下:

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()());  //"The Window"(在非嚴格模式下)

說明:這里的this是指window,那為什么不是取得其包含作用域(或外部作用域)的this對象呢?這是因為,每個函數在被調用時都會自動取得兩個特殊的變量:thisarguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止(就是從全局活動對象中開始一層一層向內部搜索,只要搜索到this變量,則停止搜索),因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了,如下:

var name = "The Window";

var object = {
    name : "My Object",

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

alert(object.getNameFunc()());  //"MyObject"

說明:this的指向在函數定義的時候是不能確定的,只有在函數執行的時候才能確定,實際上,this的最終指向的是那個調用它的對象。

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

推薦閱讀更多精彩內容