定義函數的方式
定義函數表達式的方法有兩種,一種是函數聲明,另一種是函數表達式.
函數聲明的方式,關於函數聲明的方式,它的一個重要的特性就是函數聲明提升(function declaration hoisting),意思是在執行代碼之前會先讀取函數聲明。這就意味着可以把函數聲明放在調用它的語句后面,like this
1 sayHi();//聲明函數(function declaration hoisting) 2 function sayHi(){ 3 alert("Hi!"); 4 }
函數表達式有多種表達方式,下面是最常見的一種
var func = function(agr1,arg2){//創建匿名函數 alert(arg1 + ' ' + arg2);//函數體 }
函數表達式與其他表達式一樣,使用前必須賦值。比如下面的代碼就會導致錯誤。
var condition = true; //never do this! 不同的瀏覽器會做出不同的行為 不要這么做! if(condition){ function sayHi(){ alert("Hi!"); } } else { function sayHi(){ alert("Yo!"); } } sayHi();
函數可以賦值給變量,自然也可以當作返回值返回。
遞歸
就像每一門語言一樣,任何方法或者函數解決問題的思路有遞歸和枚舉兩種基本思路,但是js中的遞歸還是有個小陷阱的,這里需要注意。看看下面的代碼吧,只是一個簡單的求階乘的函數。
//求階乘,看起來並沒有問題 function factorial(num) { if (num <= 1) { return 1; } else { return num * factorial(num - 1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //error!
這里隱藏的問題就是,如果先用一個變量anotherFactorial指向factorial函數對象(函數對象哦),再將fatorial置空(相當於window對象的factorial屬性=null),再執行anotherFactorial()時,會發現調用fatorial時會發生錯誤的,原因是null不是函數。那么為了避免這樣的問題我們可以這樣改進這個函數,使用arguments.callee可以解決這個問題。改進后的代碼如下:
function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } } var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //24
閉包
閉包是指有權訪問另一個函數作用域中的變量的函數。首先,閉包是一個函數,然后閉包函數有權訪問另一個函數的作用域中的變量。常見的創建閉包的方式就是在一個函數內部創建另一個函數。看看下面的代碼
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 0; }else{ return 1; } } }
要理解閉包就必須回顧下作用鏈的概念,當某個函數第一次被調用時,就會創建一個執行環境(excution context)以及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(【【Scope】】)。然后,使用this,arguments和其他命名參數的值來初始化函數的活動對象(activation object),但是在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象始終處於第三位,...以此類推,直至作於作用域鏈重點的全局執行環境。
先從一個簡單的函數分析下作用域鏈,代碼如下:
functon compare(value1,value2){ if(value1 < value2){ return -1; }else if(value1 == value2){ return 0; }else{ return 1; } }
var result = compare(5,10);
看看JS高級編程的這個圖。
后台的每個執行環境都有一個表示變量的對象--變量對象。全局環境的變量對象始終存在,而像compare()函數這樣的局部環境的變量對像,則只在函數執行的過程中存在。在創建compare()函數時(js文件被加載時,函數對象無論是否調用,只要聲明了就會作為window對象的一個屬性,但是函數表達式的方式除外),會創建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在函數內部的[[Scope]]屬性當中,當調用compare()函數時,會為函數創建一個執行環境,然后通過復制函數的[[Scope]]屬性中的對象從而構建起執行環境的作用域鏈。此后又有一個活動對象(在此作為變量對象使用)被創建並被推入執行環境作用域的前端。顯然,作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。
一般來講,函數執行完畢后,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象),但是,閉包的情況就有所不同。
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中,所以內部函數的作用域鏈實際上會包含外部函數的活動對象,這樣內部函數就可以訪問外部函數的變量了。更為重要的是外部函數執行完成后,其活動對象也不會被銷毀,因為內部匿名函數的作用鏈仍然在引用這個活動對象。換句話說,當外部函數執行完成后,外部函數的作用域鏈會被銷毀,但是外部函數的活動對象依然被內部匿名函數的作用域鏈所引用,這樣外部函數的活動對象就不會被銷毀,直到匿名函數被銷毀后(解除對活動對象的引用),外部函數的活動對象才會被銷毀。再看看下面的這個圖:
塊級作用域
JavaScript是沒有塊級作用域的的概念的,這就意味着在塊語句中定義的變量,實際上是包含在函數中而非包含在在塊語句中創建的,來看看下面的例子。
function blockTest(count){ for(var i = 0;i <= count ;i++){ console.debug(i); } //如果這是c/c++/java 那么這里應該報未定義錯誤 console.debug(i);//但是js不會 } blockTest(3);
在Java/C等語言中變量i只會在for循環的語句塊中定義,循環一旦結束,變量i就會被銷毀,而在js中並不是如此,變量i是被定義在blockTest()的活動對象中的,因此從它有定義開始,就可以在函數內部隨處訪問它,即使像下面這樣錯誤的重新聲明同一個變量,也不會改變它的值。
function outputNumbers(count) { for (var i = 0; i < count; i++) { console.debug(i); } var i; //variable re-declared console.debug(i); //count }
JavaScript從來不會告訴你是否多次聲明了同一個變量;遇到這種情況,它只會對后續的聲明視而不見。匿名函數可以用來模仿塊級作用域並且避免這個問題。
用塊級作用域(通常也稱為私有作用域)的匿名函數的語法如下所示:
(function(){ //這里是私有作用域 })();
以上代碼定義並且立即執行了一個匿名函數。將函數的聲明包含在一對圓括號中,表示它實際上是一個函數表達式。而緊隨其后的另一對圓括號會立即調用這個函數。如果覺得這個不好理解,我們可以寫成這樣
like this
var blockTest = function(){//使用函數表達式來定義一個函數 //這里是塊級作用域 } blockTest();//立即調用這個函數
//把上面的兩步寫成一步 (functio(){ //塊級作用域 })//函數表達式的方法定義一個函數 ()//立即執行這個函數
這種利用匿名函數表達式來實現塊級作用域的技術常常被用在函數的外部,從而限制向全局作用域中添加過多的變量和函數,一般來說,我們都應該盡量少的向全局作用域添加變量和函數。在一個由很多人員參與開發的大型應用程序中,過多的全局變量和函數很容易導致命名沖突。而通過創建私有作用域,每個開發人員既可以使用自己的變量,而又不用擔心搞亂全局作用域。
私有變量
特權函數構造器模式
name屬性在每個對象中都有一份,不會引起共享錯誤,getName和setName方法是用閉包形式訪問name變量,這樣避免了外部環境對Person的name變量的訪問,只能通過函數對Person的name變量訪問
function Person(name){ this.getName = function(){ return name; }; this.setName = function(value){ name = value; }; } var person = new Person('Nicholas'); console.debug(person.getName()); person.setName('kobe Bryant'); console.debug(person.getName());
或者name屬性不作為參數,而是作為構造函數內的一個變量,這樣對於外部環境是封閉不可見的,如果想要訪問name屬性(變量),可以使用閉包的形式訪問,like this
//構造函數模式實現私有變量(通過閉包實現) function Person(nameStr){ var name = nameStr;//私有變量 this.age = 15;//公有變量 this.getName = function(){ return name; }; this.setName = function(value){ name = value; }; this.getInfo = function(){ console.debug(name + " " +this.age); }; } var p1 = new Person('Lebron'); var p2 = new Person('Paul'); p1.getInfo(); p2.getInfo(); p1.setName('kobe Bryant'); p1.getInfo(); p2.getInfo();
但是這種構造器模式每次調用時都會創建函數對象,耗費資源,函數不可以共享。
靜態私有變量模式
通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法,基本模式如下
//靜態私有變量 (function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction() { return false; } //構造函數 MyObject = function(){ }; //公有/特權方法 MyObject.prototype.publicMethod = function(){ privateVariable++; return privateFunction(); } })();
下面的代碼中的這個模式創建了一個私有作用域,並在其中封裝了一個構造函數及相應的方法。在私有作用域中首先定義了私有變量和私有函數,並且在定義構造函數時沒有使用函數聲明而是使用了函數表達式的方式,因為函數聲明只能創建局部函數,但我們需要在外部訪問這個構造函數,所以MyObject()就成了一個全局變量,在私有作用域的外部也可以訪問。需要注意的是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"); console.debug(person1.getName()); //"Nicholas" person1.setName("Greg"); console.debug(person1.getName()); //"Greg" var person2 = new Person("Michael"); console.debug(person1.getName()); //"Michael" console.debug(person2.getName()); //"Michael"
單例對象的模塊模式(module pattern)
上面所說的方法都是為自定義類型私有化變量/函數的,那么對於一個對象而言或者說單例對象而言,如何實現其屬性或者函數的私有化呢?先來看看不需要實現對象內屬性私有化的對象該如何定義
var singleton = { name : 'value', method : function(){ //方法代碼 } };
對象是以對象字面量的方式定義的,如果想要隱藏或者說私有化對象的屬性或者方法那么選擇使用閉包的形式,也就是道格拉斯所提出的模塊模式(module pattern),其語法就像是這樣
//道格拉斯所說的模塊(module pattern ) var singleton = function(){ //私有變量和私有函數 var privateVariable = 20; function privateFunction(){ return false; } //特權/共有方法和屬性 return { publicProperty : true, publicMethod : function(){ privateVariable++; return privateFunction(); } } }();
模塊模式適用於要私有化變量,並且在初始化時需要做一定操作,並且在私有化屬性的同時開放訪問接口的情況使用看看下面的例子
function BaseComponent(){ } console.debug("====================="); 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); } } }; }(); console.debug(application.getComponentCount());
增強的模塊模式
進一步增強的模塊模式是指在返回對象志強對其增強功能,這種增強的模塊模式適合那些單例必須是某種類型的實例。就像下面這樣
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; }(); alert(application instanceof BaseComponent); application.registerComponent(new OtherComponent()); alert(application.getComponentCount()); //2
ss