zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JS有名函数表达式全面解析

JS 函数 解析 全面 表达式 有名
2023-06-13 09:14:17 时间

Example#1:Functionexpressionidentifierleaksintoanenclosingscope

实例1:函数表达式标示符渗进了外围作用域

varf=functiong(){};
typeofg;//"function"

RememberhowImentionedthatanidentifierofnamedfunctionexpressionisnotavailableinanenclosingscope?Well,JScriptdoesn"tagreewithspecsonthisone-gintheaboveexampleresolvestoafunctionobject.Thisisamostwidelyobserveddiscrepancy.It"sdangerousinthatitinadvertedlypollutesanenclosingscope-ascopethatmightaswellbeaglobalone-withanextraidentifier.Suchpollutioncan,ofcourse,beasourceofhard-to-trackbugs.

我刚才提到过一个有名函数表达式的标示符不能在外部作用域中被访问。但是,JScript在这点上和标准并不相符,在上面的饿例子中g却是一个函数对象。这个是一个可以广泛观察到的差异。这样它就用一个多余的标示符污染了外围作用域,这个作用域很有可能是全局作用域,这样是很危险的。当然这个污染可能是一个很难去处理和跟踪的bug的根源
Example#2:NamedfunctionexpressionistreatedasBOTH-functiondeclarationANDfunctionexpression

实例2:有名函数表达式被进行了双重处理,函数表达式和函数声明

typeofg;//"function"
varf=functiong(){};

AsIexplainedbefore,functiondeclarationsareparsedforemostanyotherexpressionsinaparticularexecutioncontext.TheaboveexampledemonstrateshowJScriptactuallytreatsnamedfunctionexpressionsasfunctiondeclarations.Youcanseethatitparsesgbeforean“actualdeclaration”takesplace.

正如我前面解释的,在一个特定的执行环境中,函数声明是在所有的表达式之前被解释。上面的例子说明JScript实际上把有名函数表达式作为一个函数声明来对待。我们可以看到他在一个实际的声明之前就被解释了。

Thisbringsustoanextexample:

在此基础上我们引入了下面的一个例子。
Example#3:NamedfunctionexpressioncreatesTWODISCTINCTfunctionobjects!

实例3:有名函数表达式创建两个不同的函数对象。

varf=functiong(){};
f===g;//false

f.expando="foo";
g.expando;//undefined

Thisiswherethingsaregettinginteresting.Orrather-completelynuts.Hereweareseeingthedangersofhavingtodealwithtwodistinctobjects-augmentingoneofthemobviouslydoesnotmodifytheotherone;Thiscouldbequitetroublesomeifyoudecidedtoemploy,say,cachingmechanismandstoresomethinginapropertyoff,thentriedaccessingitasapropertyofg,thinkingthatitisthesameobjectyou"reworkingwith.

在这里事情变得更加有趣了,或者是完全疯掉。这里我们看到必须处理两个不同的对象的危险,当扩充他们当中的一个的时候,另外一个不会相应的改变。如果你打算使用cache机制并且在f的属性中存放一些东西,只有有试图在g的属性中访问,你本以为他们指向同一个对象,这样就会变得非常麻烦

Let"slookatsomethingabitmorecomplex.

让我们来看一些更复杂的例子。
Example#4:Functiondeclarationsareparsedsequentiallyandarenotaffectedbyconditionalblocks

实例4:函数声明被顺序的解释,不受条件块的影响

varf=functiong(){
return1;
};
if(false){
f=functiong(){
return2;
}
};
g();//2

Anexamplelikethiscouldcauseevenhardertotrackbugs.Whathappenshereisactuallyquitesimple.First,gisbeingparsedasafunctiondeclaration,andsincedeclarationsinJScriptareindependentofconditionalblocks,gisbeingdeclaredasafunctionfromthe“dead”ifbranch-functiong(){return2}.Thenallofthe“regular”expressionsarebeingevaluatedandfisbeingassignedanother,newlycreatedfunctionobjectto.“dead”ifbranchisneverenteredwhenevaluatingexpressions,sofkeepsreferencingfirstfunction-functiong(){return1}.Itshouldbeclearbynow,thatifyou"renotcarefulenough,andcallgfromwithinf,you"llendupcallingacompletelyunrelatedgfunctionobject.

像这样的一个例子可能会使跟踪bug非常困难。这里发生的问题却非常简单。首先g被解释为一个函数声明,并且既然JScript中的声明是和条件块无关的,g就作为来自于已经无效的if分支中的函数被声明functiong(){return2}。之后普通的表达式被求值并且f被赋值为另外一个新创建的函数对象。当执行表达式的时候,由于if条件分支是不会被进入的,因此f保持为第一函数的引用functiong(){return1}。现在清楚了如果不是很小心,而且在f内部调用g,你最终将调用一个完全无关的g函数对象。

Youmightbewonderinghowallthismesswithdifferentfunctionobjectscomparestoarguments.callee.Doescalleereferenceforg?Let"stakealook:

你可能在想不从的函数对象和arguments.callee相比较的结果会是怎样呢?callee是引用f还是g?让我们来看一下

varf=functiong(){
return[
arguments.callee==f,
arguments.callee==g
];
};
f();//[true,false]

Asyoucansee,arguments.calleereferencessameobjectasfidentifier.Thisisactuallygoodnews,asyouwillseelateron.

我们可以看到arguments.callee引用的是和f标示符一样的对象,就像稍后你会看到的,这是个好消息

LookingatJScriptdeficiencies,itbecomesprettyclearwhatexactlyweneedtoavoid.First,weneedtobeawareofaleakingidentifier(sothatitdoesn"tpolluteenclosingscope).Second,weshouldneverreferenceidentifierusedasafunctionname;Atroublesomeidentifierisgfromthepreviousexamples.Noticehowmanyambiguitiescouldhavebeenavoidedifweweretoforgetaboutg"sexistance.Alwaysreferencingfunctionviaforarguments.calleeisthekeyhere.Ifyouusenamedexpression,thinkofthatnameassomethingthat"sonlybeingusedfordebuggingpurposes.Andfinally,abonuspointistoalwayscleanupanextraneousfunctioncreatederroneouslyduringNFEdeclaration.

既然看到了JScript的缺点,我们应该避免些什么就非常清楚了。首先,我们要意识到标示符的渗出(以使得他不会污染外围作用域)。第二点,我们不应该引用作为函数名的标示符;从前面的例子可以看出g是一个问题多多的标示符。请注意,如果我们忘记g的存在,很多歧义就可以被避免。通常最关键的就是通过f或者argument.callee来引用函数。如果你使用有名的表达式,记住名字只是为了调试的目的而存在。最后,额外的一点就是要经常清理有名函数表达式声明错误创建的附加函数

Ithinklastpointneedsabitofanexplanation:

我想最有一点需要一些更多解释
JScript内存管理

BeingfamiliarwithJScriptdiscrepancies,wecannowseeapotentialproblemwithmemoryconsumptionwhenusingthesebuggyconstructs.Let"slookatasimpleexample:

熟悉了JScript和规范的差别,我们可以看到当使用这些有问题的结构的时候,和内存消耗相关的潜在问题

varf=(function(){
if(true){
returnfunctiong(){};
}
returnfunctiong(){};
})();

Weknowthatafunctionreturnedfromwithinthisanonymousinvocation-theonethathasgidentifier-isbeingassignedtoouterf.Wealsoknowthatnamedfunctionexpressionsproducesuperfluousfunctionobject,andthatthisobjectisnotthesameasreturnedfunction.Thememoryissuehereiscausedbythisextraneousgfunctionbeingliterally“trapped”inaclosureofreturningfunction.Thishappensbecauseinnerfunctionisdeclaredinthesamescopeasthatpeskygone.Unlessweexplicitlybreakreferencetogfunctionitwillkeepconsumingmemory.

我们发现从匿名调用中返回的一个函数,也就是以g作为标示符的函数,被复制给外部的f。我们还知道有名函数表达式创建了一个多余的函数对象,并且这个对象和返回的对象并不是同一个函数。这里的内存问题就是由这个没用的g函数在一个返回函数的闭包中被按照字面上的意思捕获了。这是因为内部函数是和可恶的g函数在同一个作用域内声明的。除非我们显式的破坏到g函数的引用,否则他将一直占用内存。

varf=(function(){
varf,g;
if(true){
f=functiong(){};
}
else{
f=functiong(){};
}
//给g赋值null以使他不再被无关的函数引用。
//null`g`,sothatitdoesn"treferenceextraneousfunctionanylonger

g=null;
returnf;
})();

Notethatweexplicitlydeclaregaswell,sothatg=nullassignmentwouldn"tcreateaglobalgvariableinconformingclients(i.e.non-JScriptones).Bynullingreferencetog,weallowgarbagecollectortowipeoffthisimplicitlycreatedfunctionobjectthatgrefersto.

注意,我们又显式的声明了g,所以g=null赋值将不会给符合规范的客户端(例如非JScirpt引擎)创建一个全局变量。通过给g以null的引用,我们允许垃圾回收来清洗这个被g所引用的,隐式创建的函数对象。

WhentakingcareofJScriptNFEmemoryleak,Idecidedtorunasimpleseriesofteststoconfirmthatnullinggactuallydoesfreememory.

当考虑JScript的有名函数表达式的内存泄露问题时,我决定运行一系列简单的测试来证实给g函数null的引用实际上可以释放内存
测试

Thetestwassimple.Itwouldsimplycreate10000functionsvianamedfunctionexpressionsandstoretheminanarray.Iwouldthenwaitforaboutaminuteandcheckhowhighthememoryconsumptionis.AfterthatIwouldnull-outthereferenceandrepeattheprocedureagain.Here"satestcaseIused:

这个测试非常简单。他将通过有名函数表达式创建1000个函数,并将它们储存在一个数组中。我等待了大约一分钟,并查看内存使用有多高。只有我们加上null引用,重复上述过程。下面就是我使用的一个简单的测试用例

functioncreateFn(){
return(function(){
varf;
if(true){
f=functionF(){
return"standard";
}
}
elseif(false){
f=functionF(){
return"alternative";
}
}
else{
f=functionF(){
return"fallback";
}
}
//varF=null;
returnf;
})();
}

vararr=[];
for(vari=0;i<10000;i++){
arr[i]=createFn();
}

ResultsasseeninProcessExploreronWindowsXPSP2were:

结果是在WindowsXPSP2进行的,通过进程管理器得到的

IE6:

without`null`:7.6K->20.3K
with`null`:7.6K->18K

IE7:

without`null`:14K->29.7K
with`null`:14K->27K

Theresultssomewhatconfirmedmyassumptions-explicitlynullingsuperfluousreferencedidfreememory,butthedifferenceinconsumptionwasrelativelyinsignificant.For10000functionobjects,therewouldbea~3MBdifference.Thisisdefinitelysomethingthatshouldbekeptinmindwhendesigninglarge-scaleapplications,applicationsthatwillrunforeitherlongtimeorondeviceswithlimitedmemory(suchasmobiledevices).Foranysmallscript,thedifferenceprobablydoesn"tmatter.

结果在一定程度上证实了我的假设,显示的给无用的参考以null值确实会释放内存,但是在内寸的消耗的区别上貌似不是很大。对于1000个函数对象,大约应该有3M左右的差别。但是有一些是明确的,在设计大规模的应用的时候,应用要不就是要运行很长时间的或者要在一个内存有限的设备上(例如移动设备)。对于任何小的脚本,差别可能不是很重要。

Youmightthinkthatit"sallfinallyover,butwearenotjustquitethereyet:)There"satinylittledetailthatI"dliketomentionandthatdetailisSafari2.x

你可以认为这样就可以结束了,但是还没到结束的时候。我还要讨论一些小的细节,而且这些细节是在Safari2.x下的
Safaribug

EvenlesswidelyknownbugwithNFEispresentinolderversionsofSafari;namely,Safari2.xseries.I"veseensomeclaimsonthewebthatSafari2.xdoesnotsupportNFEatall.Thisisnottrue.Safaridoessupportit,buthasbugsinitsimplementationwhichyouwillseeshortly.

虽然没有被人们发现在早期的Safari版本,也就是Safari2.x版本中有名函数表达式的bug。但是我在web上看到一些声称Safari2.x根本不支持有名函数表达式。这不是真的。Safari的确支持有名函数表达式,但是稍后你将看到在它的实现中是存在bug的

Whenencounteringfunctionexpressioninacertaincontext,Safari2.xfailstoparsetheprogramentirely.Itdoesn"tthrowanyerrors(suchasSyntaxErrorones).Itsimplybailsout:

在某些执行环境中遇到函数表达式的时候,Safari2.x将解释程序整体失败。它不抛出任何的错误(例如SyntaxError)。展示如下

(functionf(){})();//<==有名函数表达式NFE
alert(1);//因为前面的表达式是的整个程序失败,本行将无法达到,thislineisneverreached,sincepreviousexpressionfailstheentireprogram

Afterfiddlingwithvarioustestcases,IcametoconclusionthatSafari2.xfailstoparsenamedfunctionexpressions,ifthosearenotpartofassignmentexpressions.Someexamplesofassignmentexpressionsare:

在用一些测试用例测试之后,我总结出,如果有名函数表达式不是赋值表达式的一部分,Safari解释有名函数表达式将失败。一些赋值表达式的例子如下

//变量声明partofvariabledeclaration
varf=1;

//简单的赋值partofsimpleassignment
f=2,g=3;

//返回语句partofreturnstatement
(function(){
return(f=2);
})();

ThismeansthatputtingnamedfunctionexpressionintoanassignmentmakesSafari“happy”:

这就意味着把有名函数表达式放到赋值表达式中会让Safari非常“开心”

(functionf(){});//fails失败

varf=functionf(){};//works成功

(function(){
returnfunctionf(){};//fails失败
})();

(function(){
return(f=functionf(){});//works成功
})();

setTimeout(functionf(){},100);//fails

Italsomeansthatwecan"tusesuchcommonpatternasreturningnamedfunctionexpressionwithoutanassignment:

这也意味着我们不能使用这种普通的模式而没有赋值表达式作为返回有名函数表达式


//要取代这种Safari2.x不兼容的情况Insteadofthisnon-Safari-2x-compatiblesyntax:
(function(){
if(featureTest){
returnfunctionf(){};
}
returnfunctionf(){};
})();

//我们应该使用这种稍微冗长的替代方法weshouldusethisslightlymoreverbosealternative:
(function(){
varf;
if(featureTest){
f=functionf(){};
}
else{
f=functionf(){};
}
returnf;
})();

//或者另外一种变形oranothervariationofit:
(function(){
varf;
if(featureTest){
return(f=functionf(){});
}
return(f=functionf(){});
})();

/*
Unfortunately,bydoingso,weintroduceanextrareferencetoafunction
whichgetstrappedinaclosureofreturningfunction.Topreventextramemoryusage,
wecanassignallnamedfunctionexpressionstoonesinglevariable.
不幸的是这样做我们引入了对函数的另外一个引用
他将被包含在返回函数的闭包中
为了防止多于的内存使用,我们可以吧所有的有名函数表达式赋值给一个单独的变量
*/

var__temp;

(function(){
if(featureTest){
return(__temp=functionf(){});
}
return(__temp=functionf(){});
})();

...

(function(){
if(featureTest2){
return(__temp=functiong(){});
}
return(__temp=functiong(){});
})();

/*
Notethatsubsequentassignmentsdestroypreviousreferences,
preventinganyexcessivememoryusage.
注释:后面的赋值销毁了前面的引用,防止任何过多的内存使用
*/

IfSafari2.xcompatibilityisimportant,weneedtomakesure“incompatible”constructsdonotevenappearinthesource.Thisisofcoursequiteirritating,butisdefinitelypossibletoachieve,especiallywhenknowingtherootoftheproblem.

如果Safari2.x的兼容性非常重要。我们需要保证不兼容的结构不再代码中出现。这当然是非常气人的,但是他确实明确的可以做到的,尤其是当我们知道问题的根源。

It"salsoworthmentioningthatdeclaringafunctionasNFEinSafari2.xexhibitsanotherminorglitch,wherefunctionrepresentationdoesnotcontainfunctionidentifier:

还值得一提的是在Safari中声明一个函数是有名函数表达式的时候存在另外一个小的问题,这是函数表示法不含有函数标示符(估计是toString的问题)

varf=functiong(){};

//Noticehowfunctionrepresentationislacking`g`identifier
String(g);//function(){}

Thisisnotreallyabigdeal.AsIhavealreadymentionedbefore,functiondecompilationissomethingthatshouldnotberelieduponanyway.

这不是个很大的问题。因为之前我已经说过,函数反编译在任何情况下都是不可信赖的。
解决方案

varfn=(function(){

//声明一个变量,来给他赋值函数对象declareavariabletoassignfunctionobjectto
varf;

//条件的创建一个有名函数conditionallycreateanamedfunction
//并把它的引用赋值给fandassignitsreferenceto`f`
if(true){
f=functionF(){}
}
elseif(false){
f=functionF(){}
}
else{
f=functionF(){}
}

//给一个和函数名相关的变量以null值Assign`null`toavariablecorrespondingtoafunctionname
//这可以使得函数对象(通过标示符的引用)可以被垃圾收集所得到Thismarksthefunctionobject(referredtobythatidentifier)
//availableforgarbagecollection
varF=null;

//返回一个条件定义的函数returnaconditionallydefinedfunction
returnf;
})();

Finally,here"showwewouldapplythis“techinque”inreallife,whenwritingsomethinglikeacross-browseraddEventfunction:

最后,当我么一个类似于跨浏览器addEvent函数的类似函数时,下面就是我们如何在真实的应用中使用这个技术


//1)用一个分离的作用域封装声明enclosedeclarationwithaseparatescope
varaddEvent=(function(){

vardocEl=document.documentElement;

//2)声明一个变量,用来赋值为函数declareavariabletoassignfunctionto
varfn;

if(docEl.addEventListener){

//3)确保给函数一个描述的标示符makesuretogivefunctionadescriptiveidentifier
fn=functionaddEvent(element,eventName,callback){
element.addEventListener(eventName,callback,false);
}
}
elseif(docEl.attachEvent){
fn=functionaddEvent(element,eventName,callback){
element.attachEvent("on"+eventName,callback);
}
}
else{
fn=functionaddEvent(element,eventName,callback){
element["on"+eventName]=callback;
}
}

//4)清除通过JScript创建的addEvent函数cleanup`addEvent`functioncreatedbyJScript
//保证在赋值之前加上varmakesuretoeitherprependassignmentwith`var`,
//或者在函数顶端声明addEventordeclare`addEvent`atthetopofthefunction
varaddEvent=null;

//5)最后通过fn返回函数的引用finallyreturnfunctionreferencedby`fn`
returnfn;
})();
可替代的解决方案

It"sworthmentioningthatthereactuallyexistalternativewaysof
havingdescriptivenamesincallstacks.Waysthatdon"trequireoneto
usenamedfunctionexpressions.Firstofall,itisoftenpossibleto
definefunctionviadeclaration,ratherthanviaexpression.Thisoption
isonlyviablewhenyoudon"tneedtocreatemorethanonefunction:

需要说明,实际上纯在一个种使得在调用栈上显示描述名称(函数名)的替代方法。一个不需要使用有名函数表达式的方法。首先,通常可以使用声明而不是
使用表达式来定义函数。这种选择通常只是适应于你不需要创建多个函数的情况。

varhasClassName=(function(){

//定义一些私有变量definesomeprivatevariables
varcache={};

//使用函数定义usefunctiondeclaration
functionhasClassName(element,className){
var_className="(?:^|\\s+)"+className+"(?:\\s+|$)";
varre=cache[_className]||(cache[_className]=newRegExp(_className));
returnre.test(element.className);
}

//返回函数returnfunction
returnhasClassName;
})();


Thisobviouslywouldn"tworkwhenforkingfunctiondefinitions.
Nevertheless,there"saninterestingpatternthatIfirstseenusedby
TobieLangel.Thewayitworksisbydefiningallfunctions
upfrontusingfunctiondeclarations,butgivingthemslightlydifferent
identifiers:

这种方法显然对于多路的函数定义不适用。但是,有一个有趣的方法,这个方法我第一次在看到Tobie
Langel.在使用。这个用函数声明定义所有的函数,但是给这个函数声明以稍微不同的标示符。

varaddEvent=(function(){

vardocEl=document.documentElement;

functionaddEventListener(){
/*...*/
}
functionattachEvent(){
/*...*/
}
functionaddEventAsProperty(){
/*...*/
}

if(typeofdocEl.addEventListener!="undefined"){
returnaddEventListener;
}
elseif(typeofdocEl.attachEvent!="undefined"){
returnattachEvent;
}
returnaddEventAsProperty;
})();


Whileit"sanelegantapproach,ithasitsowndrawbacks.First,by
usingdifferentidentifiers,youloosenamingconsistency.Whetherit"s
goodorbadthingisnotveryclear.Somemightprefertohaveidentical
names,whileotherswouldn"tmindvaryingones;afterall,different
namescanoften“speak”aboutimplementationused.Forexample,seeing
“attachEvent”indebugger,wouldletyouknowthatitisanattachEvent-basedimplementationofaddEvent.Ontheotherhand,
implementation-relatednamemightnotbemeaningfulatall.Ifyou"re
providinganAPIandname“inner”functionsinsuchway,theuserofAPI
couldeasilygetlostinalloftheseimplementationdetails.

虽然这是一个比较优雅的方法,但是他也有自己的缺陷。首先,通过使用不同的标示符,你失去的命名的一致性。这是件好的事情还是件坏的事情还不好说。
有些人希望使用一支的命名,有些人则不会介意改变名字;毕竟,不同的名字通常代表不同的实现。例如,在调试器中看到“attachEvent”,你就可以
知道是addEvent基于attentEvent的一个实现。另外一方面,和实现相关的名字可能根本没有什意义。如果你提供一个api并用如此方法命名
内部的函数,api的使用者可能会被这些实现细节搞糊涂。

Asolutiontothisproblemmightbetoemploydifferentnaming
convention.Justbecarefulnottointroduceextraverbosity.Some
alternativesthatcometomindare:

解决这个问题的一个方法是使用不同的命名规则。但是注意不要饮用过多的冗余。下面列出了一些替代的命名方法

`addEvent`,`altAddEvent`and`fallbackAddEvent`
//or
`addEvent`,`addEvent2`,`addEvent3`
//or
`addEvent_addEventListener`,`addEvent_attachEvent`,`addEvent_asProperty`


Anotherminorissuewiththispatternisincreasedmemory
consumption.Bydefiningallofthefunctionvariationsupfront,you
implicitlycreateN-1unusedfunctions.Asyoucansee,ifattachEventisfoundindocument.documentElement,
thenneitheraddEventListenernoraddEventAsPropertyareeverreallyused.Yet,they
alreadyconsumememory;memorywhichisneverdeallocatedforthesame
reasonaswithJScript"sbuggynamedexpressions-bothfunctionsare
“trapped”inaclosureofreturningone.

这种模式的另外一个问题就是增加了内存的开销。通过定义所有上面的函数变种,你隐含的创建了N-1个函数。你可以发现,如果attachEvent
在document.documentElement中发现,那么addEventListener和addEventAsProperty都没有被实际
用到。但是他们已经消耗的内存;和Jscript有名表达式bug的原因一样的内存没有被释放,在返回一个函数的同时,两个函数被‘trapped‘在闭
包中。

Thisincreasedconsumptionisofcoursehardlyanissue.Ifalibrary
suchasPrototype.jswastousethispattern,therewouldbenotmore
than100-200extrafunctionobjectscreated.Aslongasfunctionsare
notcreatedinsuchwayrepeatedly(atruntime)butonlyonce(atload
time),youprobablyshouldn"tworryaboutit.

这个递增的内存使用显然是个严重的问题。如果和Prototype.js类似的库需要使用这种模式,将有另外的100-200个多于的函数对象被创
建。如果函数没有被重复地(运行时)用这种方式创建,只是在加载时被创建一次,你可能就不用担心这个问题。