1. 程式人生 > >函式表示式,遞迴,作用域,作用域鏈,閉包,閉包與變數

函式表示式,遞迴,作用域,作用域鏈,閉包,閉包與變數

函式表示式

函式表示式是JavaScript中的一個既強大又容易令人困惑的特性。定義函式的方式有兩種:一種是函式宣告(沒錯,不同於C語言之類的),另一種就是函式表示式。

函式申明(這相當於C語言的函式定義)的語法是這樣的:

function functionName(arg0,arg1,arg2){

//函式體

}

首先是function關鍵字,然後是函式的名字,這就是指定函式名的方式。Firefox、Safari、Chrome和Opera都給函式定義了一個非標準的name屬性,通過這個屬性可以訪問到給函式指定的名字。這個屬性的值永遠等於跟在function關鍵字後面的識別符號。

//只在Firefox、Safari、Chrome和Opera有效

alert(functionName.name);                       //“functionName”

關於函式宣告,它的一個重要特徵就是函式申明提升,意思是在執行程式碼之前會先讀取函式宣告。這就意味著可以把函式宣告放在呼叫它的語句後面。(神奇~先呼叫後定義?嘿嘿,這就是太沉陷於C語言了哦~)

sayHi();

function sayHi(){

 alert("Hi~");

}

這個例子不會丟擲錯誤,因為在程式碼執行之前會先讀取函式宣告。

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

var functionName=function(arg0,arg1,arg2){

//函式體

};

這種形式看起來好像是常規的變數賦值語句,即建立一個函式並將它賦值給變數functionName。這種情況下建立的函式叫做匿名函式,因為function關鍵字後面沒有識別符號(你給它起的名字)。(匿名函式有時候也叫拉姆達函式。)匿名函式的name屬性是空字串。

函式表示式與其他表示式一樣,在使用前必須先賦值。以下程式碼會導致錯誤。

sayHi();

var sayHi=function(){

alert("Hi~");

};

理解函式提升的關鍵,就是理解函式申明與函式表示式之間的區別。例如,執行以下程式碼的結果可能會讓人意想不到。

//不要這麼做!

if(condition){

function sayHi(){

alert("Hi!");

  }

} else {

function sayHi(){

alert("Yo!");

   }

}

表面上看,以上程式碼表示在condition為true時,使用一個sayHi()的定義;否則,就使用另一個定義。實際上,這在ECMAScript中屬於無效語法,JavaScript引擎會嘗試修正錯誤,將其轉換為合理的狀態。但問題是瀏覽器嘗試修正錯誤的做法並不一致。大多數瀏覽器會返回第二個宣告,忽略condition;Firefox會在condition為true時返回第一個宣告。因此這種使用方式很危險,不應該出現在你的程式碼中

不過,如果是使用函式表示式,那就沒有什麼問題了。

//可以這樣做

var sayHi;

if(condition){

   sayHi=function(){

       alert("Hi~");

  };

} else {

      sayHi=function(){

           alert("Yo~");

    };

}

這個例子不會有什麼意外,不同的函式會根據condition被賦值給sayHi.

能夠建立函式再賦值給變數,也就能夠把函式作為其他函式的值返回。

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()函式內部,它是匿名的。在把函式當成值來使用的情況下,都可以使用匿名函式。不過,這並不是匿名函式的唯一用途。

遞迴

遞迴函式是在一個函式通過名字呼叫自身的情況下構成的,如下所示:

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變數設定為null,結果指向原始函式的引用只剩下一個。但在接下來呼叫anotherFactorial()時,由於必須執行factorial(),而factorial已經不再是函數了,所以就會導致錯誤。在這種情況下,使用arguments.callee可以解決這個問題。

我們知道,arguments.callee是一個指向正在執行的函式的指標,因此可以用它來實現對函式的遞迴呼叫,例如:

function factorial(num){

       if(num<=1){

             return 1;

    } else {

           return num*arguments.callee(num-1);

    }

}

 加粗的程式碼顯示,通過使用arguments.callee代替函式名,可以確保無論怎樣呼叫函式都不會出問題。因此,在編寫遞迴函式時,使用arguments.callee總比使用函式名更保險。

但在嚴格模式下,不能通過指令碼訪問arguments.callee,訪問這個屬性會導致錯誤。不過,可以使用命名函式表示式來達成相同的結果。例如:

var factorial=(function  f(num){

        if(num<=1){

               return 1;

  }  else {

               return num*f(num-1);

    }

});

以上程式碼建立了一個名為f()的命名函式表示式,然後將它賦值給變數factorial。即便把函式賦值給了另一個變數,函式的名字f仍然有效,所以遞迴呼叫照樣能正確完成。這種方式在嚴格模式和非嚴格模式下都行得通。

JavaScript中,JavaScript裡一切都是物件,包括函式。函式物件和其它物件一樣,擁有可以通過程式碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是作用域,包含了函式被建立的作用域中物件的集合,稱為函式的作用域鏈。

作用域(scope)

通常來說一段程式程式碼中使用的變數和函式並不總是可用的,限定其可用性的範圍即作用域,作用域的使用提高了程式邏輯的區域性性,增強程式的可靠性,減少名字衝突。

作用域鏈(scope chain)

作用域鏈決定了哪些資料能被函式訪問。當一個函式建立後,它的作用域鏈會被建立此函式的作用域中可訪問的資料物件填充。 

閉包

有不少開發人員總是搞不清匿名函式和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函式作用域中的變數的函式。在javascript中,只有函式內部的子函式才能讀取區域性變數,所以閉包可以理解成“定義在一個函式內部的函式“。在本質上,閉包是將函式內部和函式外部連線起來的橋樑。建立閉包的常見方式,就是在一個函式內部建立另一個函式。

舉個例子:

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;

                }

    };              //閉包

}

在這個例子中,突出的那兩行程式碼是內部函式(一個匿名函式)中的程式碼,這兩行程式碼訪問了外部函式中的變數propertyName。即使這個內部函式被返回了,而且是在其他地方被呼叫了,但它仍然可以訪問變數propertyName。之所以還能夠訪問這個變數,是因為內部函式的作用域鏈中包含createComparisonFunction()的作用域。要徹底搞清楚其中的細節,必須從理解函式被呼叫的時候都會發生什麼入手。

當某個函式被呼叫時,會建立一個執行環境及相應的作用域鏈。然後,使用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()函式,然後又在全域性作用域中呼叫了它。當呼叫compare()時,會建立一個包含arguments、value1和value2的活動物件。全域性執行環境的變數物件(包含result和compare)在compare()執行環境的作用域鏈中則處於第位。

上述例子中compare()函式執行時的作用域鏈:

後臺的每個執行環境都有一個表示變數的物件——變數物件全域性環境的變數物件始終存在,而像compare()函式這樣的區域性環境的變數物件,則只在函式執行的過程中存在。在建立compare()函式時,會建立一個預先包含全域性變數物件的作用域鏈,這個作用域鏈被儲存在內部的[[Scope]](中文意思:作用域)屬性中。當呼叫compare()函式時,會為函式建立一個執行環境,然後通過複製函式的[[Scope]]屬性中的物件構建起執行環境的作用域鏈。此後,又有一個活動物件(在此作為變數物件使用)被建立並被推入執行環境作用域鏈的前端。對於這個例子中compare()函式執行環境而言,其作用域鏈中包含兩個變數物件:本地活動物件和全域性變數物件。顯然,作用域鏈本質上是一個指向變數物件的指標列表,它只引用但不實際包含變數物件。

無論什麼時候在函式中訪問一個變數時,就會從作用域鏈中搜索具有相應名字的變數。一般來講,當函式執行完畢後,區域性活動物件就會被銷燬,記憶體中僅儲存全域性作用域(全域性執行環境的變數物件)。但是,閉包的情況又有所不同。

在另一個函式內部定義的函式會將包含函式(即外部函式)的活動物件新增到它的作用域鏈中。因此,在createComparisonFunction()函式內部定義的匿名函式的作用域鏈中,實際上將會包含外部函式createComparisonFunction()的活動物件。

在匿名函式從createComparisonFunction()中被返回後,它的作用域鏈被初始化為包含createComparisonFunction()函式的活動物件和全域性變數物件。這樣,匿名函式就可以訪問在createComparisonFunction()中定義的所有變數。更為重要的是,createComparisonFunction()函式在執行完畢後,其活動物件也不會被銷燬,因為匿名函式的作用域鏈仍然在引用這個活動物件。換句話說,當createComparisonFunction()函式返回後,其執行環境的作用域鏈會被銷燬,但它的活動物件仍然會留在記憶體中;直到匿名函式被銷燬後,createComparisonFunction()的活動物件才會被銷燬,例如:

//建立函式

var compareNames=createComparisonFunction("name");        // compareNames是匿名函式

//呼叫函式

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

//解除對匿名函式的引用(以便釋放記憶體)

compareNames=null;

首先,建立的比較函式被儲存在變數compareNames中。而通過將compareNames設定為等於null解除該函式的引用,就等於通知垃圾回收例程將其清除。隨著匿名函式的作用域鏈被銷燬,其他作用域(除了全域性作用域)也都可以安全地銷燬了。

呼叫compareNames()的過程中產生的作用域鏈之間的關係如下:

閉包與變數

作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函式中任何變數的最後一個值。別忘了閉包所儲存的是整個變數物件,而不是某個特殊的變數。

舉個例子:

function createFunctions(){

  var result=new Array();

  for(var i=0;i<10;i++){

       result[i]=function(){

            return i;

          };

    }

return result;

}

這個函式會返回一個函式陣列。表面上看,似乎每個函式都應該返回自己的索引值,即位置0的函式返回0,位置1的函式返回1,以此類推。但實際上,每個函式都返回0。因為每個函式的作用域鏈中都儲存著createFunctions()函式的活動物件,所以它們引用的都是同一個變數i。當createFunctions()函式返回後,變數i的值是10,此時每個函式都引用著儲存變數i的同一個變數物件,所以在每個函式內部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;

}

在重寫了前面的createFunctions()函式後,每個函式就會返回各自不同的索引值了。在這個版本中,我們沒有直接把閉包賦值給陣列,而是定義了一個匿名函式,並將立即執行該匿名函式的結果賦給陣列。這裡的匿名函式有一個引數num,也就是最終的函式要返回的值。在呼叫每個匿名函式時,我們傳入了變數i。由於函式引數是按值傳遞的,所以就會將變數i的當前值複製給引數num。而在這個匿名函式內部,又建立並返回了一個訪問num的閉包。這樣一來,result陣列中的每個函式都有自己num變數的一個副本,因此就可以返回各自不同的數值了。