鬆鬆個人網絡日誌
函數表達式
1.遞歸
函數通過名字調用自身。使用callee屬性解決(指向擁有arguments屬性的函數,該函數),嚴格模式下無法使用function factorial(num){ if(num <= 1){ return 1; }else{ return num*factorial(num-1); } } var anotherFactorial = factorial; var factorial = null; //factorial賦值null後會導致遞歸調用出錯(factorial is not a function) console.log(anotherFactorial(4));
使用命名函數表達式來實現function factorial(num){ if(num <= 1){ return 1; }else{ return num*arguments.callee(num-1); } }
創建一個命名爲f的函數表達式,然後賦值給變量factorialfunction factorial(function f()){ if(num <= 1){ return 1; }else{ return num*f(num-1); } }
2.閉包
指有權訪問另一個函數作用域中的變量的函數,創建閉包常見方式:在一個函數內部創建另一個函數在匿名函數中訪問了外部函數的propertyName參數,即使這個匿名函數被返回了,並且是在其他地方被調用,仍然可以訪問到該參數。因爲內部函數的作用域鏈包含了createComarisonFunction()的作用域。function createComparisonFunction(propertyName){ //返回一個匿名函數 return function(obj1,obj2){ var value1 = obj1[propertyName]; var value2= obj2[propertyName]; if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } }; }
當某個函數第一次被調用時,會創建一個執行環境(execution context),及相應的作用域鏈,並把作用域鏈賦值給一個特殊的內部屬性(即[[Scope]]),然後使用this,arguments和其他命名參數的值來初始化函數的活動對象(activation object)。但在作用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,直到作用域鏈的終點--全局執行環境。在函數執行過程中,爲讀取和寫入變量的值,就需要在作用域鏈中查找變量第一次調用compare()時,會創建一個包含this,arguments,value1和value2的活動對象。全局執行環境的變量對象包含this,arguments和compare。在compare()執行環境的作用域鏈中處於第二位。function compare(value1,value2){ if(value1<value2){ return -1; return 1; }else { return 0; } var result = compare(1,2);//在全局作用域中調用compare()函數
如上圖所示:作用域鏈本質上是一個指向變量對象的指針列表,只引用但不實際包含變量對象。無論什麼時候在函數中訪問一個變量時,就會從作用域鏈中搜索具有相應名稱的變量。一般當函數執行完後,局部活動對象就被銷燬了,內存中僅保存全局作用域(全局執行環境的變量對象)。閉包除外!
在函數內部定義的函數會將外部函數的活動對象添加到它的作用域鏈中。
因此,在createComparisonFunction()函數內部定義的匿名函數的作用域鏈中,實際上將會包含外部函數createComparisonFunction()的活動對象。
//創建函數 var compare = createComparisonFunction("name"); //調用函數 var result = compare({name:"Ethan"},{name:"Zhangsan"}); //解除對匿名函數的引用(以便垃圾回收,釋放內存) compare = null;
閉包與變量
由於閉包會攜帶包含它的函數的作用域,因此會比其他函數佔用更多的內存。過度使用閉包可能會導致內存佔用過多,建議只在絕對必要時再考慮使用閉包。問題:閉包只能取得包含函數中任何變量的最後一個值,閉包所保存的是整個變量對象,而不是某個特殊的變量。
createFunction()函數返回一個函數數組,每個函數作用域鏈中都保存着createFunction()函數的活動對象,所以它們引用的是同一個變量i。當createFunction()函數返回後,變量i的值是10,所以每個函數內部i的值都是10。function createFunction(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(){//聲明10個函數 return i; } } return result; } var functionArr = createFunction(); functionArr.forEach(function(item,index){ console.log(item()); // 10 輸出十次,每次都是同一個結果 });
通過創建另一個匿名函數強制讓閉包行爲符合預期。
定義一個匿名函數,並將立即執行該匿名函數的結果賦給數組。匿名函數有一個參數num。在調用每個匿名函數時,都傳入參數變量i。由於函數參數(基本類型)是按值傳遞的,所以會將變量i的當前值複製給參數num。在這個匿名函數內部,又創建並返回了一個訪問num的閉包。這樣,result數組中的每個函數都有自己num變量的一個副本,因此返回的就是各自不同的數值了。function createFunction(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(num){ //創建並立刻執行當前匿名函數 return function(){//返回一個函數 return num; } }(i); } return result; } var functionArr = createFunction(); functionArr.forEach(function(item,index){ console.log(item()); // 10 輸出十次,每次都是同一個結果 });
this對象
this對象是在運行時基於函數的執行環境綁定的:在全局函數中,this等於window,而當函數被作爲某個對象的方法調用時,this等於那個對象。匿名的執行環境具有全局性,this對象通常指向window。
var name = "Window"; var object = { name : "Ethan", getNameFun:function(){ return function(){//返回一個匿名函數結果 return this.name;//this指向window } } }; console.log(object.getNameFun()()); // Window
每個函數在被調用時,其活動對象都會自動取得兩個特殊變量:this和arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止(內部函數自己的this對象),因此永遠不可能直接訪問外部函數中的這兩個變量,但是把外部作用域中的this對象保存在一個閉包能夠訪問到的變量裏,就可以讓閉包訪問該對象了。
在定義匿名函數前將this賦值給outThis變量,定義閉包後,閉包也可以訪問這個變量。即使函數返回後,outThis也引用着object。var name = "Window"; var object = { name : "Ethan", getNameFun:function(){ var outThis = this;//將外部函數的this對象賦值給匿名函數能訪問到的變量 return function(){//返回一個匿名函數結果 return outThis.name;//訪問的外部函數的name屬性 } } }; console.log(object.getNameFun()()); // Ethan
因此:如果想訪問外部函數作用域中的this和arguments對象,必須將該對象的引用保存到另一個閉包能訪問的變量中。特殊情況:第三種調用方法中:先執行賦值語句,然後再調用賦值後的結果。賦值表達式的值是函數本身,this的值不能維持,所以返回window。var name = "window"; var object = { name:"Ethan", getName:function(){ return this.name; } }; console.log(object.getName()); // Ethan (object.getName)();//Ethan (object.getName = object.getName)(); // window
內存泄漏
如果閉包中保存着html元素(com對象使用的引用計數機制回收垃圾),那就以爲着該元素將無法被銷燬。
在匿名函數中保存了對保存了對外部函數的活動對象的引用,只要匿名函數存在,element的引用數至少是1。因此它所佔用的內存將無法被回收。function assignHandler(){ var element = document.getElementById("hehe"); element.onclick = function(){ alert(element.id); } }
解決方法: 手動斷開對html元素的引用。
function assignHandler(){ var element = document.getElementById("hehe"); var id = element.id; element.onclick = function(){ alert(element.id); } //函數執行完後,閉包的作用域鏈依然會引用外部函數的整個活動對象(活動對象中有element引用)。所以需手動將html元素的引用置空。 element = null; }
3.模仿塊級作用域
JS中沒有塊級作用域的概念。所以在塊語句中定義的變量,實際上是包含在函數中而非語句中的。
for(var i=0;i<10;i++){ console.log(i); } alert(i); //10,仍然可以訪問,這裏i是在全局作用域中的 var i;//多次聲明同一變量,JS會忽視後面的聲明 alert(i);//10
使用閉包解決:
var name = "Ethan"; (function(){//匿名函數,私有作用域,執行完後變量i被銷燬 for(var i=0;i<10;i++){ console.log(i); } console.log(name); //匿名函數是一個閉包,可以訪問包含作用域中的變量 })(); alert(i); //報錯,i未定義
()會將function(){} 由函數聲明變成函數表達式,在後面加上()就會立刻調用這個匿名函數。
這種做法可以減少閉包占用的內存問題,因爲沒有指向匿名函數的引用。只要函數執行完畢,就可以立即銷燬其作用域鏈了。
4.私有變量
一:在構造函數中定義特權方法JS中沒有私有成員的概念,所有對象屬性都是公有的。但是在函數中定義的變量,都可以認爲是私有變量,因爲不能在函數外部訪問這些變量。私有變量包括函數的參數,局部變量和在函數內部定義的其他函數。有權訪問私有變量和私有函數的公有方法叫做特權方法(privileged method),有兩種在對象上創建特權方法的方式。
缺點:構造函數模式,每個實例都會創建同樣的一組新方法。function Person(name){ this.getName = function(){//閉包,通過作用域鏈可訪問包含函數的name參數 return name; }; this.setName = function(value){ name = value; } } var p = new Person("Ethan"); console.log(p.getName());// Ethan p.setName("Zhangsan"); console.log(p.getName()); // Zhangsan
改進:靜態私有變量,通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法
與在構造函數中定義的區別:私有變量和函數是所有實例共享的,特權方法在原型上定義,所有實例調用的都是同一個函數。而這個特權函數作爲一個閉包,總是保存着對包含作用域的引用(外部匿名函數--私有作用域)(function(){ var privateVariable = 10; //聲明私有變量 function privateFunction(){//聲明私有函數 return false; } // function MyObject(){ 如果使用函數聲明的方式定義構造函數,就成私有函數了,無法被外部訪問 // } //構造函數 MyObject = function(){//省略var,使用表達式方式給構造函數賦值,可以在全局被調用。 }; //特權方法,在構造函數原型上定義,可被多個實例對象共享 MyObject.prototype.publicMethod = function(){ privateVariable ++; return privateFunction(); } })();
(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("Ethan"); console.log(person1.getName()); //Ethan person1.setName("Zhangsan"); console.log(person1.getName()); //Zhangsan var person2 = new Person("Collen"); console.log(person1.getName()); //Collen console.log(person2.getName()); //Collen
name成了一個靜態,由所有實例共享的屬性。在一個實例上調用setName()會影響所有的實例。
多查找作用域鏈中的一個層次,就會在一定程度上影響查找速度。正式使用閉包和私有變量的一個明顯不足之處。
二:模塊模式
前面的模式是用於爲自定義類型創建私有變量和特權方法的。而模塊模式則是爲單例創建私有變量和特權方法。
如果必須創建一個對象並以某些數據對其進行初始化,同時還要公開一些能夠訪問這些私有數據的方法,就可以使用模塊模式。這種模式創建的每個單例都是Object的實例var singleton = function(){ //私有變量和私有函數 var privateVariable = 10; function privateFunction(){ return false; } //特權方法和屬性 return {//返回一個對象 publicVariable : 20, publicFunction:function(){ privateVariable++; //操作私有變量 return privateFunction();//返回調用私有方法的結果,false } }; }(); console.log(singleton.publicFunction()); //false console.log(singleton.publicVariable); //20
增強的模塊模式
這種模式適合那些單例必須是某種類型的實例,同時還必須添加某些屬性和方法對其加以增強的情況。
var application = function(){ //私有變量和函數 var components = new Array(); //初始化 components.push(new BaseComponent()); //創建application的一個局部副本 var app = new BaseComponent(); //公共接口 app.getComponentCount = function(){ return components.length; }; app.registerComponent = function(component){ if(typeof component == "object"){ components.push(component); } } return app; }();
小結:
在函數內部定義了其他函數時,就創建了閉包。閉包有權訪問包含函數內部的所有變量。
- 在後臺執行環境中,閉包的作用域鏈包含着它自己的作用域,包含函數的作用域和全局作用域
- 通常,函數的作用域及其所有變量都會在函數執行結束後被銷燬
- 但是,當函數返回了一個閉包時,這個函數的作用域將會一直保存到閉包不存在爲止
注意:創建閉包必須維護額外的作用域,所以過度使用它可能會佔用大量內存