javascript高級編程之函數(shù)表達式 遞歸和閉包函數(shù)
定義函數(shù)表達式有兩種方式:函數(shù)聲明和函數(shù)表達式.
函數(shù)聲明如下:
function functionName(arg0,arg1,arg2){ //函數(shù)體 }
首先是function關(guān)鍵字,然后是函數(shù)的名字.
FF,Safrai,Chrome和Opera都給函數(shù)定義了一個非標準的name屬性,通過這個屬性可以訪問到函數(shù)指定的名字.這個函數(shù)的值永遠等于跟在function關(guān)鍵字后面的標識符.
//只在FF,Safari,Chrome和Opera有效 alert(functionName.name)//functionName
函數(shù)聲明的特征就是函數(shù)聲明提升(function declaration hoisting),意思是在執(zhí)行代碼之前會先讀取函數(shù)聲明.這就意味著可以把函數(shù)聲明放在調(diào)用它的語句后面.
sayHi(); function sayHi(){ alert("Hi!"); }
這種例子不會拋出錯誤,因為在代碼執(zhí)行之前會先讀取函數(shù)聲明.
第二種是函數(shù)表達式.
var functionName=function(arg0,arg0,arg2){ //函數(shù)體 }
這種形式看起來好像是常規(guī)的變量賦值語句,即創(chuàng)建一個函數(shù)并將它賦值給變量functionName.這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù)(anonymous function),因為function關(guān)鍵字后面沒有標識符.(匿名函數(shù)有時候也叫拉姆達函數(shù).)匿名函數(shù)的name屬性是空字符串.
函數(shù)表達式與其他表達式一樣,在使用前必須先賦值.
以下代碼會導致錯誤:
syaHi();//Uncaught ReferenceError: syaHi is not defined var sayHi=function(){ alert("Hi!"); }
不要像下面這樣寫代碼,這在ECMAScript中屬于無效語法,JavaScript引擎會嘗試修正錯誤,但不同瀏覽器修改不同.
//不要這樣做 if(condition){ function sayHi(){ alert("Hi!"); } }else{ function sayHi(){ alert("Yo!"); } }
如果是使用函數(shù)表達式,就沒什么問題了.
//可以這樣做 var sayHi; if(condition){ sayHi=function(){ alert("Hi!"); } }else{ sayHi=function(){ alert("Yo!"); } }
能夠創(chuàng)建函數(shù)再賦值給變量,也就能夠把函數(shù)作為其它函數(shù)的值返回.
function creatComparisonFunction(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; } }; }
creatComparisonFunction()就返回了一個匿名函數(shù).返回的函數(shù)可能會被賦值給一個變量,或者以其他方式被調(diào)用;不過,在creatComparisonFunction()函數(shù)內(nèi)部,它是匿名的.在把函數(shù)當成值來使用的情況下,都可以使用匿名函數(shù).
7.1 遞歸
遞歸函數(shù)是一個函數(shù)通過名字調(diào)用自己的情況下構(gòu)造的.
function factorial(num){ if(num<=1){ return 1; }else{ return num*factorial(num-1); } }
上面是一個經(jīng)典的遞歸階乘函數(shù).下面的代碼卻可能導致它出錯.
var anotherFactorial=factorial; factorial=null; alert(anotherFactorial(4));//Uncaught TypeError: factorial is not a function
以上代碼先把factorial()函數(shù)保存在變量anotherFactorial中,之后又將factorial變量設為null,結(jié)果指向原始引用只剩下一個.接下來調(diào)用anotherFactorial()時,由于必須執(zhí)行factorial(),而factorial()已經(jīng)不再是函數(shù),所以會導致錯誤.
這種情況下,使用arguments.callee可以解決.
arguments.callee是一個指向正在執(zhí)行的函數(shù)的指針,因此可以用它來實現(xiàn)對函數(shù)的遞歸調(diào)用.
function factorial(num){ if(num<=1){ return 1; }else{ return num*arguments.callee(num-1); } }
在編寫遞歸函數(shù)時,使用arguments.callee總比使用函數(shù)名更保險,因為它可以確保無論怎么調(diào)用函數(shù)都不會出問題.
但在嚴格模式下,不能通過腳本訪問arguments.callee.
不過可以使用函數(shù)表達式來達成相同的結(jié)果.
var factorial=(function f(num){ if(num<=1){ return 1; }else{ return num*f(num-1); } }); console.log(factorial(4));//24
7.2 閉包
閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù).創(chuàng)建閉包的常見方式,就是在一個函數(shù)內(nèi)部創(chuàng)建另一個函數(shù).
function creatComparisonFunction(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; } }; }
加粗的兩行代碼是內(nèi)部函數(shù)(一個匿名函數(shù))中的代碼,這兩行代碼訪問了外部函數(shù)中的變量propertyName.即使這個內(nèi)部函數(shù)被返回了,而且是在其他地方被調(diào)用了,但它仍然可以訪問變量propertyName.之所以還能夠訪問這個變量,是因為內(nèi)部函數(shù)的作用域鏈中包含creatComparisonFunction()的作用域.
當某個函數(shù)被調(diào)用時,會創(chuàng)建一個執(zhí)行環(huán)境(execution context)及相應的作用域鏈.然后,使用arguments和其他命名參數(shù)的值來初始化函數(shù)的活動對象(activation object).但在作用域鏈中,外部函數(shù)的活動對象始終處于第二位,外部函數(shù)的外部函數(shù)的活動對象處于第三位,....直至作為作用域鏈終點的全局執(zhí)行環(huán)境.
在函數(shù)執(zhí)行過程中,為讀取和寫入變量的值,就需要姑作用域鏈中查找變量.
function compare(value1,value2){ if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } } var result=compare(5,10) console.log(result)//-1
以上代碼先定義了compare()函數(shù),然后又在全局作用域中調(diào)用了它.當調(diào)用compare()時,會創(chuàng)建一個包含arguments,value1,value2的活動對象.全局執(zhí)行環(huán)境的變量對象(包含result和compare)在compare()執(zhí)行環(huán)境的作用域鏈中則處于第二位
后臺的每個執(zhí)行環(huán)境都有一個表示變量的對象--變量對象.全局環(huán)境的變量對象始終存在,而像compare()函數(shù)這樣的局部環(huán)境的變量對象,則只在函數(shù)執(zhí)行的過程中存在.在創(chuàng)建compare()函數(shù)時,會創(chuàng)建一個預先包含全局變量對象的作用域鏈,這個作用域鏈被保存在內(nèi)部的[[Scope]]屬性中.當調(diào)用compare()函數(shù)時,會為函數(shù)創(chuàng)建一個執(zhí)行環(huán)境,然后通過復制函數(shù)的[[Scope]]屬性中的對象構(gòu)建起執(zhí)行環(huán)境的作用域鏈.此后,又有一個活動對象(在此作為變量對象使用)被創(chuàng)建并被推入執(zhí)行環(huán)境作用域鏈的前端.
作用域鏈本質(zhì)上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象.
無論什么時候在函數(shù)中訪問一個變量時,就會從作用域鏈中搜索具有相應名字的變量.一般來講,當函數(shù)執(zhí)行完畢后,局部活動對象就會被銷毀,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對象).但是,閉包的情況又有所不同.
在另一個函數(shù)內(nèi)部定義的函數(shù)會將包含函數(shù)(即外部函數(shù))的活動對象添加到它的作用域中.
var compare=creatComparisonFunction("name"); var result=comapre({name:"Nicholas"},{name:"Greg"});
下圖展示了上面代碼代碼執(zhí)行時,包含函數(shù)與內(nèi)部匿名函數(shù)的作用域.
當createComparisonFunction()函數(shù)返回后,其執(zhí)行環(huán)境的作用域會被銷毀,但它的活動對象仍然會留在內(nèi)存中;直到匿名函數(shù)被銷毀后,createComparisonFunction()的活動對象都會被銷毀.
//創(chuàng)建函數(shù) var compare=creatComparisonFunction("name"); //調(diào)用函數(shù) var result=comapre({name:"Nicholas"},{name:"Greg"}); //解除對匿名函數(shù)的引用(以便釋放內(nèi)存) compareNames=null;
通過將compareNames設置為等于null解除該函數(shù)的引用,就等于通知垃圾回收例程將其清除.隨著匿名函數(shù)函數(shù)的作用域鏈被銷毀,其他作用域(除了全局作用域)也都可以安全地銷毀了.
由于閉包會攜帶包含它的函數(shù)的作用域,因此會比其他函數(shù)占用更多的內(nèi)存.過度使用閉包可能會導致內(nèi)存占用過多,慎重使用閉包.
7.2.1 閉包和變量
作用域鏈的這種配置的機制引出了一個值得注意的副作用,即閉包只能取得包含函數(shù)中任何變量的最后一個值.
閉包里所保存的是整個變量對象,而不是某個特殊的變量.
function createFunctions(){ var result=new Array(); for(var i=0;i<10;i++){ result[i]=function(){ return i; }; } return result; }
上面代碼里這個函數(shù)會返回一個函數(shù)數(shù)組.表面上看,似乎每個函數(shù)都應該返回自己的索引值,但實際上,每個函數(shù)都返回10.因為每個函數(shù)的作用域鏈中都保存著createFunctions()函數(shù)的活動對象,所以它們引用的都是同一個變量i.當createFunction()函數(shù)返回后,變量i的值是10,此時每個函數(shù)都引用著保存變量i的同一個變量對象,所以在每個函數(shù)內(nèi)部i的值都是10.
但是,我們可以通過創(chuàng)建另一個匿名函數(shù)強制讓閉包的行為符合預期.
function createFunctions(){ var result=new Array(); for(var i=0;i<10;i++){ result[i]=function(num){ return function(){ return num; } }(i); } return result; }
重寫之后,每個函數(shù)就會返回各自不同的索引值了.在這個版本中,我們沒有直接把閉包賦值給數(shù)組,而是定義了一個匿名函數(shù),并將立即執(zhí)行匿名函數(shù)的結(jié)果賦給數(shù)組.這里的匿名函數(shù)有一個參數(shù)num,也就是最終的函數(shù)要返回的值.在調(diào)用每個匿名函數(shù)時,我們傳入了變量i.由于函數(shù)參數(shù)是按值傳遞的,所以就會將變量i的當前值復制給參數(shù)num.而在這個匿名函數(shù)內(nèi)部,又創(chuàng)建并返回了一個訪問num的閉包.這個一來,result數(shù)組中的每個函數(shù)都有自己num變量的一個副本,因此就可以返回各自不同的數(shù)值了.
7.2.2 關(guān)于this對象
this對象是在運行時基本函數(shù)的執(zhí)行環(huán)境綁定的:在全局函數(shù)中,this等于window,而當函數(shù)被作為某個對象的方法調(diào)用時,this等于那個對象.不過,匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其this對象通常指向window.
var name="the window"; var object={ name:"my object", getNameFunc:function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()());//the window(在非嚴格模式下)
每個函數(shù)在被調(diào)用時都會自動取得兩個特殊變量:this和arguments.內(nèi)部函數(shù)在搜索這兩個變量時,只會搜索到其活動對象為止,因此永遠不可能直接訪問外部函數(shù)中的這兩個變量.
不過,把外部作用域中的this對象保存在一個閉包能夠訪問到的變量里,就可以讓閉包訪問該對象了.
var name="the window"; var object={ name:"my object", getNameFunc:function(){ var that=this; return function(){ return that.name; }; } }; alert(object.getNameFunc()());//my object
在定義匿名函數(shù)之前,我們把this對象賦值給了一個名叫that的變量.而在定義了閉包之后,閉包也可以訪問這個變量,因為它是我們在包含函數(shù)中特意聲明的一個變量.即使在函數(shù)返回之后,that也仍然引用著object,所以調(diào)用object.getNameFunc()()就返回了my object.
注意:this和arguments也存在同樣的問題.如果想訪問作用域中的arguments對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中.
var name="the window"; var object={ name:"my object", getName:function(){ return this.name; } }; console.log(object.getName());//my object console.log((object.getName)());//my object console.log((object.getName=object.getName)());//the window
最后一行代碼先執(zhí)行了一條賦值語句,然后再調(diào)用賦值后的結(jié)果.因為這個賦值表達式的值是函數(shù)本身,所以this的值不能得到維持,結(jié)果就返回了"this window".
7.2.3 內(nèi)存泄露
由于IE9之前的版本對JScript對象和COM對象使用不同的垃圾收集例程,因此閉包在IE的這些版本中會導致一些特殊的問題.具體來說,如果閉包的作用域鏈中保存著一個HTML元素,那么就意味著該元素將無法被銷毀.
function assignHandlet(){ var element=document.getElementById("someElement"); element.onclick=function(){ alert(element.id); }; }
以上代碼創(chuàng)建了一個作為element元素事件處理程序的閉包,而這個閉包則又創(chuàng)建了一個循環(huán)引用.由于匿名函數(shù)保存了一個對assignHandler()的活動對象的引用,因此應付導致無法減少element的引用數(shù).只要匿名函數(shù)存在,element的引用數(shù)至少也是1,因此它所占用的內(nèi)存就永遠不會被回收.不過這個問題可以通過稍微改寫一下代碼來解決.
function assignHandlet(){ var element=document.getElementById("someElement"); var id=element.id; element.onclick=function(){ alert(id); }; element=null; }
上面代碼中,通過把element.id的一個副本保存在一個變量中,并且在閉包中引用該變量消除了循環(huán)引用.
腳本之家友情提醒大家:閉包會引用包含函數(shù)的整個活動對象,而其中包含著element.即使閉包不直接引用element,包含函數(shù)的活動對象也仍然會保存一個引用.因此,有必要把element變量設置為null.這樣就能夠解除對DOM對象的引用,順利地減少其引用數(shù),確保正?;厥掌湔加玫膬?nèi)存.
下面給大家介紹下函數(shù)表達式。
在JavaScript 編程中,函數(shù)表達式是一種非常有用的技術(shù)。使用函數(shù)表達式可以無須對函數(shù)命名,從而實現(xiàn)動態(tài)編程。匿名函數(shù),也稱為拉姆達函數(shù),是一種使用JavaScript 函數(shù)的強大方式。以下總結(jié)了函數(shù)表達式的特點。
函數(shù)表達式不同于函數(shù)聲明。函數(shù)聲明要求有名字,但函數(shù)表達式不需要。沒有名字的函數(shù)表達式也叫做匿名函數(shù)。
在無法確定如何引用函數(shù)的情況下,遞歸函數(shù)就會變得比較復雜;遞歸函數(shù)應該始終使用arguments.callee 來遞歸地調(diào)用自身,不要使用函數(shù)名——函數(shù)名可能會發(fā)生變化。
當在函數(shù)內(nèi)部定義了其他函數(shù)時,就創(chuàng)建了閉包。閉包有權(quán)訪問包含函數(shù)內(nèi)部的所有變量,原理
如下。
在后臺執(zhí)行環(huán)境中,閉包的作用域鏈包含著它自己的作用域、包含函數(shù)的作用域和全局作用域。通常,函數(shù)的作用域及其所有變量都會在函數(shù)執(zhí)行結(jié)束后被銷毀。
但是,當函數(shù)返回了一個閉包時,這個函數(shù)的作用域?qū)恢痹趦?nèi)存中保存到閉包不存在為止。
使用閉包可以在JavaScript 中模仿塊級作用域(JavaScript 本身沒有塊級作用域的概念),要點如下。
創(chuàng)建并立即調(diào)用一個函數(shù),這樣既可以執(zhí)行其中的代碼,又不會在內(nèi)存中留下對該函數(shù)的引用。
結(jié)果就是函數(shù)內(nèi)部的所有變量都會被立即銷毀——除非將某些變量賦值給了包含作用域(即外部作用域)中的變量。
閉包還可以用于在對象中創(chuàng)建私有變量,相關(guān)概念和要點如下。即使JavaScript 中沒有正式的私有對象屬性的概念,但可以使閉包來實現(xiàn)公有方法,而通過公有方法可以訪問在包含作用域中定義的變量。
有權(quán)訪問私有變量的公有方法叫做特權(quán)方法。
可以使用構(gòu)造函數(shù)模式、原型模式來實現(xiàn)自定義類型的特權(quán)方法,也可以使用模塊模式、增強的模塊模式來實現(xiàn)單例的特權(quán)方法。
JavaScript 中的函數(shù)表達式和閉包都是極其有用的特性,利用它們可以實現(xiàn)很多功能。不過,因為創(chuàng)建閉包必須維護額外的作用域,所以過度使用它們可能會占用大量內(nèi)存。
相關(guān)文章
Extjs gridpanel 中的checkbox(復選框)根據(jù)某行的條件不能選中的解決方法
這篇文章主要介紹了Extjs gridpanel 中的checkbox(復選框)根據(jù)某行的條件不能選中的解決方法,需要的朋友可以參考下2017-02-02Javascript中document.referrer隱藏來源的方法
這篇文章主要介紹了Javascript中document.referrer隱藏來源的方法,文中通過一個實例給大家介紹了實現(xiàn)的方法,有需要的朋友可以參考借鑒,下面來一起學習學習吧。2017-01-01