函數表達式是定義函數的一種方式,另一種是之前提到的函數聲明。
//函數聲明 function functionName(args){ //函數體 } //函數表達式 var functionName = function(args){ //函數體 };
函數聲明和函數表達式之間的區別,主要是函數聲明提升,意思是在執行代碼之前會讀取函數聲明。
沒有名字的函數表達式也叫匿名函數。
一、遞歸
遞歸是一個函數通過名字調用自身。
因為函數名可能會發生改變,如果函數內部調用自身的語句仍使用原來的名字,就會發生錯誤,在這種情況下,使用arguments.callee可以解決這個問題。arguments.callee是一個指向正在執行的函數的指針。
function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } }
除了使用arguments.callee,還可以使用命名函數表達式來達成同樣的結果。
var factorial = (funtion f(num) { if num (num <= 1){ return 1; } else { return num * f(num-1); } });
二、閉包
閉包是指有權訪問另一個函數作用域中的變量的函數。
關於作用域:當某個函數被調用時,會創建一個執行環境及對應的作用域鏈。然后,使用arguments和其他命名參數的值來初始化函數的活動對象。作用域鏈就是由一組有序(按調用順序由內到外)的活動對象的引用組成。在函數執行過程中,為讀取和寫入變量的值,就需要在作用域鏈中查找變量。 作用域鏈本質上是一個指向變量對象的指針列表,他只引用但不包含實際對象。 一般來講,當函數執行完畢后,局部活動對象會被銷毀,內存中僅保存全局作用域。但是閉包的情況又有所不同。
創建閉包必須維護額外的作用域:匿名函數作為返回值在函數內部定義,如果在外部通過函數表達式引用並調用這個匿名函數,就形成了閉包。在函數執行后,因為匿名函數仍被引用,所以匿名函數的活動對象和作用域鏈依然存在,這導致匿名函數所在的函數的活動對象仍然處於被引用狀態,留在內存中。 可以通過解除對匿名函數的引用(設置引用值為null)來解決,這樣就釋放了內存。
1. 閉包與變量
閉包保存的是整個變量對象,而不是某個特殊變量。所以對於具有同一變量名的值,閉包只會取得最后一個值。關於這個問題,下面的例子可以清晰地說明:
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; }
這個函數會返回一個函數數組。表面上看,似乎數組內每個函數都應該返回自己的索引值。但實際上,每個函數都返回10,因為每個函數的作用域鏈中都保存着createFunctions()函數的活動對象,所以他們引用的都是用一個變量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; }
每循環一次,外層的匿名函數就執行一次,將索引值傳入當前位置的函數里(雖然都叫num,但是每個num都是按值傳遞,都只是一個副本)。
2. 關於this對象
每個函數在被調用時都會自動取得兩個特殊變量:this和arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數中的這兩個變量。
this對象是在運行時基於函數的執行環境綁定的。雖然匿名函數在某個函數內定義,但一般匿名函數都是在全局函數中執行,所以其this對象通常指向window。
如果想訪問外部作用域的this對象,可以先在外部函數中將this的值保存在一個閉包能訪問到的變量里。
3. 內存泄漏
如果閉包創建了一個循環引用,就會導致無法減少變量的引用數,因此占用的內存就永遠不會回收。
三、 模仿塊級作用域
JS中只有執行環境(函數作用域)的概念,而沒有塊級作用域的概念。所以如果想實現塊級作用域,就要通過匿名函數來模仿。具體步驟是先定義一個函數,然后立即調用它。這樣即可以執行其中的代碼,又不會在內存中留下對該函數的引用。結果就是函數內部的所有變量都會立即被銷毀。
因為函數聲明后面不能跟圓括號,所以我們選擇使用函數表達式的方式。代碼如下:
(function(){ //這里是塊級作用域 })();
四、私有變量
JS中沒有正式的私有對象屬性的概念,但可以使用閉包來實現公有方法,而通過公有方法可以訪問在包含作用域中定義的變量(私有變量)。
任何在函數中定義的變量,都是私有變量,因為不能在函數的外部訪問這些變量。私有變量包括函數的參數、局部變量和在函數內部定義的其他函數。
特權方法是有權訪問私有變量和私有函數的公有方法。有兩種在對象上創建特權方法的方式。第一種是在構造函數中定義特權方法,基本模式如下:
function MyObject(){ //私有變量和私有函數 //特權方法 this.func = function (){ //有權訪問私有變量和函數 }; }
但是這種方式針對每個實例都會創建同樣一組新方法,而是用私有靜態變量來實現特權方法可以避免這個問題。
1. 靜態私有變量
在私有作用域中定義私有變量和函數,並省略var構造全局函數及下及相應的特權方法。
(function(){ var name = ""; Person = function(value){ name = value; }; Person.prototype.getName = function(){ return name; }; Person.prototype.setName = function (value){ name = value; }; })();
2. 模塊模式
前面的模式是用於為自定義類型創建私有變量和特權方法的。模塊模式則視為單例創建私有變量和特權方法。單例,指只有一個實例的對象。
在私有作用域中定義私有變量和函數,並返回公有變量和特權方法,由單例接收。
function BaseComponent(){ } function OtherComponent(){ } var application = function(){ //private variables and functions var components = new Array(); //initialization components.push(new BaseComponent()); //public interface return { getComponentCount : function(){ return components.length; }, registerComponent : function(component){ if (typeof component == "object"){ components.push(component); } } }; }();
3. 增強的模塊模式
和模塊模式類似,不同的是顯式創建要返回的對象,再返回之前可以添加某些屬性和方法對其加以增強。這種模式適合那些單例必須是某種類型的實例的情況。
function BaseComponent(){ } function OtherComponent(){ } 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; }();
