JavaScript中函数声明与函数表达式的区别详解
前言
在ECMAScript中,有两个最常用的创建函数对象的方法,即使用函数表达式或者使用函数声明。对此,ECMAScript规范明确了一点,即是,即函数声明必须始终带有一个标识符(Identifier),也就是我们所说的函数名,而函数表达式则可以省略。下面看看这两者的详细区别介绍。
什么是FunctionDeclaration(函数声明)?
FunctionDeclaration可以定义命名的函数变量,而无需给变量赋值。FunctionDeclaration是一种独立的结构,不能嵌套在非功能模块中。可以将它类比为VariableDeclaration(变量声明)。就像VariableDeclaration必须以“var”开头一样,FunctionDeclaration必须以“function”开头。
举例来说
functionbar(){ return3; }
ECMA5(13.0)定义语法:
functionIdentifier(FormalParameterList[opt]){FunctionBody}
函数名在自身作用域和父作用域内是可获取的(否则就取不到函数了)。
functionbar(){ return3; } bar()//3 bar//function
什么是FunctionExpression(函数表达式)?
FunctionExpression将函数定义为表达式语句(通常是变量赋值)的一部分。通过FunctionExpression定义的函数可以是命名的,也可以是匿名的。FunctionExpression不能以“function”开头(下面自调用的例子要用括号将其括起来)。
举例来说
//anonymousfunctionexpression vara=function(){ return3; } //namedfunctionexpression vara=functionbar(){ return3; } //selfinvokingfunctionexpression (functionsayHello(){ alert("hello!"); })();
ECMA5(13.0)定义语法:
functionIdentifieropt(FormalParameterList[opt]){FunctionBody}
(这个定义感觉并不完整,因为它忽略了一个条件:外围语句是表达式,并且不以“function”开头)
函数名(如果有的话)在作用域外是不可获取的(与FunctionDeclaration对比)。
那FunctionStatement是什么?
FunctionStatement有时是FunctionDeclaration的另一种说法。但是kangax指出,在mozilla中,FunctionStatement是FunctionDeclaration的一种拓展,使得FunctionDeclaration语句可以在任何允许使用statement(语句)的地方使用。但是FunctionStatement现在还不是标准,所以不建议应用在产品开发中。
下面我们从一些小测试开始。猜猜以下情况都会弹出什么结果?
题1:
functionfoo(){ functionbar(){ return3; } returnbar(); functionbar(){ return8; } } alert(foo());
题2:
functionfoo(){ varbar=function(){ return3; }; returnbar(); varbar=function(){ return8; }; } alert(foo());
题3:
alert(foo()); functionfoo(){ varbar=function(){ return3; }; returnbar(); varbar=function(){ return8; }; }
题4:
functionfoo(){ returnbar(); varbar=function(){ return3; }; varbar=function(){ return8; }; } alert(foo());
如果你的答案不是8、3、3和[TypeError:barisnotafunction]的话,就继续往下读吧……(即使答对了也要继续读哦)
现在来解释下前面的测试。
Question1用了functiondeclaration,也就是说它们gethoisted(被提升)了……
等一下,什么是Hoisting?
这里引用BenCherry的话:“Functiondeclaration和functionvariable(函数变量)通常会被JavaScript解释器移(‘hoisted')到当前作用域顶部”。
functiondeclaration被提升时,整个函数体都会随之提升,所以Question1的代码经过解释器解释后是像这样运行的:
//**SimulatedprocessingsequenceforQuestion1** functionfoo(){ //definebaronce functionbar(){ return3; } //redefineit functionbar(){ return8; } //returnitsinvocation returnbar();//8 } alert(foo());
但是,我们经常被告诉说,return语句后面的代码是运行不到的啊……
执行JavaScript过程中,有Context(ECMA5将之分解为LexicalEnvironment、VariableEnvironment和ThisBinding)和Process(一系列按序调用的语句)两个概念。当程序进入执行域时,Declaration会造成VariableEnvironment。它们不同于Statement(比如return),也不遵循Statement的运行规则。
FunctionExpression会被提升吗?
这取决于表达式。比如Question2中的第一个表达式:
varbar=function(){ return3; };
等号左边的(varbar)是VariableDeclaration。VariableDeclaration会被提升,但是AssignmentExpression(赋值表达式)不会。所以当bar提升时,解释器会这样初始化:varbar=undefined。而函数定义本身不会被提升。
(ECMA512.2带有initialzier(初始化器)的变量是在VariableStatement执行时,由AssignmentExpression赋值的,而不是在变量被创建时。)
因此Question2的代码会按以下顺序运行:
//**SimulatedprocessingsequenceforQuestion2** functionfoo(){ //adeclarationforeachfunctionexpression varbar=undefined; varbar=undefined; //firstFunctionExpressionisexecuted bar=function(){ return3; }; //FunctioncreatedbyfirstFunctionExpressionisinvoked returnbar(); //secondFunctionExpressionunreachable } alert(foo());//3
你可能会说,这还能解释的通,但是Question3的答案错了,我在Firebug运行会报错。
把代码保存在HTML文件中,之后在Firefox上运行试试。或者在IE8、Chrome或Safari控制台中运行。显然Firebug控制台在“global(全局)”作用域(实际并不是全局的,而是特有的“Firebug”作用域——试着在Firebug控制台中运行“this==window”你就知道了)运行代码时,不会将函数提升。
Question3和Question1的逻辑相似。这次是foo函数被提升了。
Question4就很简单了,根本就没有函数提升……
可以这么说,但是如果根本没有提升的话,TypeError会是“barnotdefined”,而不是“barnotafunction”。此例中确实没有函数提升,但是有变量提升。因此bar在开始就被声明了,但是它的值并没有定义。其它代码都是按顺序执行的。
//**SimulatedprocessingsequenceforQuestion4** functionfoo(){ //adeclarationforeachfunctionexpression varbar=undefined; varbar=undefined; returnbar();//TypeError:"barnotdefined" //neitherFunctionExpressionisreached } alert(foo());
还应该注意什么?
官方是禁止在非功能模块(比如if)中使用FunctionDeclaration的。但是所有浏览器都支持,但是各自的解释方式不同。
例如下面的代码段在Firefox3.6中会抛错,因为它将FunctionDeclaration解释成了FunctionStatement(见上文),所以x没有定义。但是在IE8、Chrome5和Safari5中,会返回函数x(和标准的FunctionDeclaration一样)。
functionfoo(){ if(false){ functionx(){}; } returnx; } alert(foo());
可以看出使用FunctionDeclaration可能会引起混淆,那么它有什么优点吗?
你可能会说FunctionDeclaration很宽松啊——如果试图在声明前使用函数,提升确实可以修正顺序,以便函数可以正确调用。但是这种宽松不利于严谨的编码,从长远的角度来看,很有可能会促进而不是阻止意外的发生。毕竟,程序员按特定的顺序排列语句是有原因的。
那么还有其它理由支持FunctionExpression的吗?
你猜呢?
1)FunctionDeclaration感觉像是要模仿Java风格的方法声明,但是Java方法和JavaScript并不一样。在JavaScript中,函数是含值的living对象。Java方法仅是对元数据的存储。下面的两段代码都定义了函数,但是只有FunctionExpression看着像创建了对象。
//FunctionDeclaration functionadd(a,b){returna+b}; //FunctionExpression varadd=function(a,b){returna+b};
2)FunctionExpression用处更多。FunctionDeclaration只能作为“statement”孤立存在。它所能做的就是创建一个当前作用域下的对象变量。相比之下,FunctionExpression(根据定义)是大型结构的一部分。如果想要创建匿名函数、给prototype(原型)添加函数或是将函数用作其它对象的property(属性),都可以用FunctionExpression。每当用高阶应用,比如curry或compose,创建新的函数时都是在用FunctionExpression。FunctionExpression和FunctionalProgramming(函数式编程)分不开。
//FunctionExpression varsayHello=alert.curry("hello!");
FunctionExpression有缺点吗?
FunctionExpression创建的函数大多是匿名的。比如下面的函数是匿名的,today只是一个匿名函数的引用:
vartoday=function(){returnnewDate()}
这会有问题吗?多数情况下不会,但是就像NickFitzgerald指出的,调试匿名函数会很烦。他建议使用NamedFunctionExpressions(NFEs)作为工作区:
vartoday=functiontoday(){returnnewDate()}
但是如AsenBozhilov所说(和Kangax文档)NFEs在IE9以下无法正确执行。
结论
随意放置的FunctionDeclaration具有误导性,并且很少有(如果有的话)情况,用FunctionExpression给变量赋值无法替代FunctionDeclaration。但是如果必须使用FunctionDeclaration的话,将其放在所属作用域顶部可以减少混淆。永远不要把FunctionDeclaration放在if语句中。
说了这么多,可能在你自己的情况下,FunctionDeclaration还是很有用的。这没什么。死记教条是危险的,并且通常会造成代码拐弯抹角。更重要的是你理解了概念,这样就可以根据自身情况决定用哪种方式创建函数。以上就是本文的全部内容了,希望此文对大家在这方面有帮助。