函數表達式的特征
函數表達式是js中一種既強大又容易令人困惑的特性。定義函數的方式有兩種:一種是函數聲明,另一種是函數表達式.
function functionName(arg0,arg1,arg2){ // 函數體 } console.log(functionName.name); // functionName // Firefox,Safari,Chrome和Opera都給函數定義了一個非標准的name屬性,通過這個屬性可以訪問到給函數指定的名字。 // 這個屬性的值永遠都等於跟在function關鍵詞后面的標識符 關於函數聲明,它的一個重要特征就是 函數聲明提升 ,意思是在執行代碼之前會讀取函數聲明,這就意味着可以把函數聲明放到調用它的語句后面: sayHi(); // Hi sayYes(); // 錯誤:函數還不存在 function sayHi(){ console.log('Hi'); } var sayYes = function () { console.log('Yes'); } // 這種形式看起來好像是常規的變量賦值語句,即創建一個函數並將它賦值給變量sayYes。 // 這種情況下創建的函數叫做匿名函數。因為function 后面沒有標識符,匿名函數的name屬性是空字符串 理解函數聲明與函數表達式之間的區別: if(true){ function sayHi(){ console.log("Hi"); } }else{ function sayHi(){ console.log("Yo!"); } } // 上述代碼在ECMAScript中屬於無效語法,js引擎會嘗試修復錯誤,將其轉換為合理的狀態,但問題是瀏覽器嘗試修正錯誤的做法並不一致。 // 但是如果使用函數表達式,那就沒什么問題了 var sayHi; if(true){ sayHi = function (){ console.log("Hi"); } }else{ sayHi = function(){ console.log("Yo!"); } } sayHi(); // Hi
遞歸
遞歸函數是在一個函數通過名字調用自身的情況下構成的。
// 一個經典的遞歸階乘函數 function factorial(num){ if(num <= 1){ return 1; }else{ return num * factorial(num - 1); } } console.log(factorial(10)); // 看起來沒什么問題,但是下面的代碼卻導致它出錯: var antherFactorial = factorial; factorial = null; console.log(antherFactorial(4)) // 出錯! // 在這種情況下,使用arguments.callee可以解決這個問題。我們知道,arguments.callee是一個指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用 -------------隔斷------------------ function factorial(num){ if(num <= 1){ return 1; }else{ return num * arguments.callee(num - 1); } } var antherFactorial = factorial; factorial = null; console.log(antherFactorial(4)) // 24 // 因此,在編寫遞歸函數時,通過使用arguments.callee代替函數名,可以確保無論怎樣調用函數都不會出現問題 // 但是在嚴格模式下,不能通過腳本訪問arguments.callee,訪問這個屬性會導致出錯。不過,可以通過命名函數表達式來達成相同的效果: var ff = (function f(num){ if(num <= 1){ return 1; }else{ return num * f(num - 1); } }) console.log(ff(100)); //9.33262154439441e+157 // 以上代碼創建了一個名為f() 的命名函數表達式,然后將它賦值給變量ff。即便把函數賦值給另一個變量,函數的名字f依然有效,所以遞歸調用照樣 // 能正確完成。這種方式在嚴格模式和非嚴格模式下都行得通
閉包
有不少開發人員總是搞不清匿名函數和閉包這兩個概念,因此經常混用。閉包是指有權訪問另一個函數作用域中的變量的函數。創建閉包的常見方式
就是在一個函數內部創建另一個函數:
function createCF(propertyName){ return function(obj1,obj2){ var val1 = obj1[propertyName]; var val2 = obj2[propertyName]; // 上面兩行代碼是內部函數(一個匿名函數)中的代碼,這兩行代碼訪問了外部函數中的變量propertyName。即使這個內部函數被返回了,而且是在其他 // 地方被調用了,但它依然可訪問變量propertyName。之所以還能訪問這個變量,是因為內部函數的作用域鏈中包含createCF()的作用域 if(val1 < val2){ return -1; }else{ return 0; } } } var o1 = {age : 14}; var o2 = {age : 13}; var CF = createCF('age'); /* 有匿名函數從createCF()被返回后,它的作用域鏈被初始化為包含createCF()函數的活動對象和全局變量對象。這樣,匿名函數 就可以訪問在createCF()中定義的所以變量,更為重要的是,createCF()函數在執行完畢時,其活動對象也不會被銷毀,因為匿名函數的 作用域鏈仍然在引用這個活動對象。換句話說,當createCF()函數返回后,其執行環境的作用域鏈會被銷毀,但它的活動對象仍然會留在內存中 。直到匿名含函數被銷毀后,createCF()函數的活動對象才會被銷毀。 */ var res = CF(o1,o2); CF = null; //解除對匿名函數的引用(以便釋放內存 /* 首先,創建的比較函數被保存在變量CF中。通過將CF設置為null解除該函數的引用,就等於通知垃圾回收例程將其清除。 隨着匿名函數的作用域鏈被銷毀,其他作用域(除了全局作用域)也都可以安全的銷毀了。 */
當函數被調用時,會創建一個執行環境及相應的作用域鏈。然后使用arguments 和 其他命名參數和初始化函數的活動對象。
但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位。。。直至作為作用域終點的全局執行環境。
無論什么時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量。一般來講,當函數執行完畢后,局部活動對象就會被銷毀,內存
中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況又有所不同:
在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中。因此,在createCF()函數內部定義的匿名函數的作用域鏈中
實際上會包含外部函數createCF()的活動對象
注意:由於閉包會攜帶包含它的函數的作用域,因此會比其他和函數占用更多的內存。過度使用閉包可能會導致內存占用過多,我們建議讀者只在必要時再
考慮使用閉包
別忘了閉包所保存的是整個變量對象,而不是某個特殊的變量。 作用鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最后一個值。
function createFunctions(){ var result = []; for(var i = 0; i < 10; i++){ result[i] = function () { return i; }; } return result; } var res = createFunctions(); for( var i in res){ console.log(res[i]()); // 每個函數返回的都是10 } //當createFunctions()函數返回后,變量i的值是10,此時每個函數都引用着保存着變量i的同一個變量對象。所以在每個函數內部i的值都是10. // 但是,我們可以創建另一個匿名函數強制讓閉包的行為符合預期。 function _createFunctions(){ var result = []; for(var i = 0; i < 10; i++){ result[i] = function (num){ return function () { return num; }; }(i); } return result; } var _res = _createFunctions(); for( var i in _res){ console.log(_res[i]()); // 返回 0 - 9 } // 這個版本,我們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦值給數組。 // 這里的匿名函數有一個參數num,也就是最終的函數要返回的值。在調用每個匿名函數時,我們傳入了變量i。 // 由於函數的參數是按值傳遞的,所以就會將變量i的值賦值給參數num。而在這個匿名函數內部,又創建了一個訪問num的閉包。 // 這樣一來,result數組中的每個函數都有自己num變量的一個副本,因此就可以返回各自不同的數值了。
關於this對象
在閉包中使用this對象也可能會導致一些問題。我們直到this對象是在運行時基於函數的執行環境綁定的:
在全局函數中,this等於window
而當函數與被作為某個對象的方法調用時,this等於那個對象
不過,匿名函數的執行環境具有全局性,因此其this對象通常指向window。但有時候由於編寫閉包的方式不同,這一點可能不會那么明顯。
var name = "the window"; var object = { name : "my object", getNameFunc : function () { return function () { return this.name; } } }; console.log(object.getNameFunc()()); // the window // 為什么匿名函數沒有取得其包含作用域(或外部作用域)的this對象呢? /* 前面已經提到過,每個函數被調用時,都會自動取得兩個特殊變量:this 和 arguments 。內部函數在搜索這兩個變量時,只會搜索到其活動對象為止 因此永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部作用域的this對象保存到在一個閉包能夠訪問到的變量,就可以讓閉包訪問該對象了。 */ var name = "the window"; var object = { name : "my object", getNameFunc : function () { var that = this; return function () { return that.name; } } }; console.log(object.getNameFunc()()); //my object
注意: 閉包會引用包含函數的整個活動對象,即使閉包不直接引用,包含函數的活動對象也仍然會保存一個引用,設為null可以解決這個問題
模仿塊級作用域
(function () { //在這里面的是塊級作用域 console.log(123); })()
這種技術經常被用在函數外部,從而限制向全局作用域中添加過多的變量和函數。一般來說在一個由很多開發人員參與的大型運用程序中,過多的全局變量很容易
導致命名沖突。而創建私有作用域,每個開發人員可以使用自己的變量,又不用擔心搞亂全局作用域
私有變量
嚴格來講,js中沒有私有成員的概念;所以對象屬性都是公有的,不過,倒是有一個私有變量的概念。任何在函數中定義的變量,都可以認為是私有變量。
因此,不能在函數的外部訪問這些變量。私有變量包含函數的參數,局部變量和再函數內部定義的其他函數
function Person(name){ this.getName = function () { return name; } this.setName = function (value) { name = value; } } var person = new Person('tom'); console.log(person.getName()); // tom person.setName('bob'); console.log(person.getName()); // bob /* 以上代碼的構造函數中定義了兩個特權方法:getName()和 setName()。這兩個方法都可以在構造函數外部使用,而且都有權訪問私有變量name。 私有變量name在Person中的每個實例中都不相同,因此每次調用構造函數都會重新創建這兩個方法。不過在構造函數中定義特權方法也有一個缺點, 那就是你必須使用構造函數模式來達成這個目的。構造函數的缺點就是針對每個實例都會創建同樣一組新方法,而使用靜態私有變量來實現特權方法 就可以避免這個問題。 */ 靜態私有變量 (function () { var name = ''; Person = function (value) { name = value; } Person.prototype.getName = function () { return name; } Person.prototype.setName = function (value) { name = value; } })(); var p1 = new Person('ni'); console.log(p1.getName()); /* 以這種方式創建靜態私有變量會因為使用原型而增進代碼復用,但每個實例都沒有自己的私有變量。 多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度,而這正是使用閉包和私有變量的一個顯明的不足之處 */
模塊模式
前面的模式是用於為自定義類型創建私有變量和特權方法的。而道格拉斯所說的模塊模式,則是為單例創建私有變量和方法的。所謂單例,指的就是只有一個實例的
對象。按照慣例,js是以對象字面量的方式來創建單例對象的。
var singleton = function () { // 私有變量和函數 var privateVariable = 10; function privateFunction () { return false; } // 特權 / 公有方法和屬性 return { publicProperty : true, publicMethod : function () { privateVariable++; return privateFunction(); } } }(); console.log(singleton); // {publicProperty: true, publicMethod: ƒ} /* 這個模塊模式使用了一個返回對象的匿名函數。在這個匿名函數內部,首先定義了私有變量和函數。 然后,將一個對象字面量作為函數的值返回。由於這個對象是在匿名函數內部定義的,因此它的公有方法有權訪問私有變量和函數。從本質上來講, 這個對象字面量定義的是單例的公共接口。這種模式在需要對單例進行某些初始化,同時又需要維護其私有變量時非常有用 */ var app = function (){ var data = []; return { joinData : function (val) { data[data.length] = val; }, getData : function(){ // !!這里有閉包,私有變量的原理還是閉包! console.log(data); } } }(); app.joinData('demo'); app.getData(); /* 簡言之,如果必須創建一個對象並以某些數據對其進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,那就可以使用模塊模式 */