本文深入解析JavaScript中函数声明与函数表达式的核心区别,重点探讨函数提升机制及条件语句中的规范差异。通过实例分析命名函数表达式在调试器调用栈中的作用,揭示ES3与ES5在原型继承处理上的行为变化,并提供优化调试体验的最佳实践方案。
函数声明与函数表达式的区别
1、函数声明与函数表达式
在ECMAScript规范中,定义函数的两种主要方式是函数声明和函数表达式。尽管两者看似相似,但存在关键差异。ECMA标准明确规定:函数声明必须包含标识符(即函数名),而函数表达式中的标识符则是可选的。
函数声明的标准语法结构为:function 函数名称 (参数:可选){ 函数体 }
函数表达式的语法结构为:function 函数名称(可选)(参数:可选){ 函数体 }
由此可知,若未指定函数名称,该定义必为函数表达式。若指定了名称,则需根据上下文判断类型:当function foo(){}作为赋值语句的一部分时,它是函数表达式;若位于程序顶层或嵌套在另一个函数体内,则视为函数声明。
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();
表达式与声明之间存在细微但重要的区别。函数声明具有提升特性,即在解析阶段,无论声明位于代码何处(即使是在代码末尾),它都会在当前作用域内优先于其他表达式被解析和求值。例如,即便函数fn在alert调用之后声明,执行alert时fn已被定义,如下例所示:
alert(fn());
function fn() {
return 'Hello world!';
}
此外需注意,虽然在条件语句中使用函数声明在某些环境中可行,但这并未被ECMA标准标准化,不同浏览器可能产生不同的执行结果。因此,在条件分支中建议始终使用函数表达式,因为条件语句内部不存在块级作用域的概念。
// 千万别这样做!
// 因为有的浏览器会返回first的这个function,而有的浏览器返回的却是第二个
if (true) {
function foo() {
return 'first';
}
}
else {
function foo() {
return 'second';
}
}
foo();
// 相反,这样情况,我们要用函数表达式
var foo;
if (true) {
foo = function() {
return 'first';
};
}
else {
foo = function() {
return 'second';
};
}
foo();
函数声明的实际语法规则如下:
函数声明只能出现在程序顶层或函数体内。从语法角度看,它们不能出现在代码块(Block,即{ … })中,例如不能直接出现在if、while或for语句内部。这是因为代码块中只能包含语句(Statement),而不能包含函数声明这类源元素。另一方面,虽然表达式可以作为表达式语句出现在代码块中,但规范明确规定表达式语句不能以关键字function开头。这意味着函数表达式同样不能直接出现在语句或代码块中。
命名函数表达式及其作用
2、命名函数表达式
命名函数表达式顾名思义需要包含名称。前文提到的var bar = function foo(){};是一个有效的命名函数表达式示例。需要特别注意:该名称仅在函数内部的作用域有效,规范规定标识符不能在外层作用域中访问。
var f = function foo(){
return typeof foo; // function --->foo是在内部作用域内有效
};
// foo在外部用于是不可见的
typeof foo; // "undefined"
f(); // "function"
既然有上述限制,命名函数表达式的实际用途是什么?为什么要特意命名?
正如前文所述,命名的主要目的是简化调试过程。在调试时,如果调用栈中的每一项都能显示具体的函数名称,将极大提升调试效率,使问题定位更加直观。
提示:这里提出一个小问题:在ES3中,命名函数表达式的作用域对象也继承了 Object.prototype 的属性。这意味着仅仅是给函数表达式命名也会将 Object.prototype 中的所有属性引入到作用域中。结果可能会出人意料。
var constructor = function(){return null;}
var f = function f(){
return construcor();
}
f(); //{in ES3 环境}
该程序看起来会产生 null, 但其实会产生一个新的对象。因为命名函数表达式在其作用域内继承了 Object.prototype.constructor(即 Object 的构造函数)。就像 with 语句一样,这个作用域会因 Object.prototype 的动态改变而受到影响。幸运的是,ES5 修正了这个错误。
这种行为的一个合理的解决办法是创建一个与函数表达式同名的局部变量并赋值为 null。即使在没有错误地提升函数表达式声明的环境中,使用 var 重声明变量能确保仍然会绑定变量 g。设置变量 g 为 null 能确保重复的函数可以被垃圾回收。
var f = function g(){
return 17;
}
var g =null;
调试器中的调用栈分析
3、调试器(调用栈)中的命名函数表达式
前文提到,命名函数表达式的核心价值在于调试。具体而言,如果函数拥有名称,调试器会在调用栈中显示该名称。部分调试器(如Firebug)有时会自动为匿名函数命名并显示,使其与使用函数的上下文角色一致,但通常这些调试器仅应用简单的命名规则,价值有限。以下是一个不使用命名函数表达式的示例:
function foo(){
return bar();
}
function bar(){
return baz();
}
function baz(){
debugger;
}
foo();
// 这里我们使用了3个带名字的函数声明
// 所以当调试器走到debugger语句的时候,Firebug的调用栈上看起来非常清晰明了
// 因为很明白地显示了名称
baz
bar
foo
expr_test.html()
通过查看调用栈信息,可以清晰得知foo调用了bar,bar又调用了baz(而foo本身是在expr_test.html文档的全局作用域内被调用)。此外,还有一个令人愉悦的功能,即前文提到的Firebug为匿名表达式自动命名的功能:
function foo(){
return bar();
}
var bar = function(){
return baz();
}
function baz(){
debugger;
}
foo();
// Call stack
baz
bar() //看到了么?
foo
expr_test.html()
然而,当函数表达式变得稍复杂时,调试器便不再智能,我们只能在调用栈中看到问号:
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function(){
return baz();
};
}
else if (window.attachEvent) {
return function() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();
// Call stack
baz
(?)() // 这里可是问号哦,显示为匿名函数(anonymous function)
foo
expr_test.html()
另外,当把函数赋值给多个变量的时候,也会出现令人郁闷的问题:
function foo(){
return baz();
}
var bar = function(){
debugger;
};
var baz = bar;
bar = function() {
alert('spoofed');
};
foo();
// Call stack:
bar()
foo
expr_test.html()
此时,调用栈显示的是foo调用了bar,但实际上并非如此。产生这种问题的原因是baz和另一个包含alert(‘spoofed')的函数发生了引用交换。
归根结底,给函数表达式命名是最可靠的方法,即使用命名函数表达式。我们使用带名字的表达式重写上述示例(注意立即调用的表达式块里返回的2个函数的名字都是bar):
function foo(){
return bar();
}
var bar = (function(){
if (window.addEventListener) {
return function bar(){
return baz();
};
}
else if (window.attachEvent) {
return function bar() {
return baz();
};
}
})();
function baz(){
debugger;
}
foo();
// 又再次看到了清晰的调用栈信息了耶!
baz
bar
foo
expr_test.html()
好的,整个文章结束,大家对javascript的认识又近了一步,希望大家越来越喜欢小编为大家整理的文章,继续关注跟我学习javascript的一系列文章。