本节书摘来华章计算机出版社《JavaScript应用程序设计》一书中的第2章,第2.2节,作者:Eric Elliott 更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.2 函数声明
在JavaScript中有多种定义函数的方法,不同方法各有优缺点。
function foo() {
/* Warning: arguments.callee is deprecated.
Use with caution. Used here strictly for
illustration. */
return arguments.callee;
}
foo(); //=> [Function: foo]
在这段代码中,foo()是一个函数声明。正如在“变量提升”一节中所提到的,你不能在条件语句中进行函数声明,这点一定要注意,下面的代码中,函数声明将无效:
var score = 6;
if (score > 5) {
function grade() {
return 'pass';
}
} else {
function grade() {
return 'fail';
}
}
module('Pass or Fail');
test('Conditional function declaration.', function () {
// Firefox: Pass
// Chrome, Safari, IE, Opera: Fail
equal(grade(), 'pass',
'Grade should pass.');
});
更为糟糕的是,不同浏览器对这段代码的解读会有差异,所以,尽量避免在条件语句下进行函数声明,详情请参见“变量提升”一节。
过度使用函数声明会导致模块中出现大量无关联函数,因为函数声明没有明确定义函数的作用范围、功能职责,以及互相间的协作方式。
var bar = function () {
return arguments.callee;
};
bar(); //=> [Function](Note: It's anonymous.)
在上述例子中我们将一个函数体赋值给变量bar,这种声明方式我们称之为函数表达式。
函数表达式的优势在于,你可以像变量赋值操作那样将函数体赋值给变量,它遵循应用正常的流程控制逻辑,这意味着你可以在条件语句中声明函数表达式。
函数表达式的不足之处在于,你始终需要为函数体指派一个名称,否则所声明的函数将变为匿名函数。匿名函数在JavaScript中很容易被滥用,假设模块中所有的函数都是匿名函数,而且彼此间互有嵌套(这在事件驱动的应用中非常常见),当嵌套层级达到12层时,恰巧某个环节出了问题,经调试发现调用栈的输出呈现:
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
(Anonymous function)
很显然,调用栈没有提供给我们任何线索:
var baz = {
f: function () {
return arguments.callee;
}
};
baz.f(); // => [Function](Note: Also anonymous.)
这是函数表达式的另外一种声明方式,将匿名函数作为属性赋值给对象字面量,此时匿名函数被称为“方法字面量”,方法是指与对象绑定的函数。
方法的优势在于,可以使用对象字面量将有关联的函数归为一组。举例来说,假设你有一组控制灯泡状态的函数:
var lightBulbAPI = {
toggle: function () {},
getState: function () {},
off: function () {},
on: function () {},
blink: function () {}
};
将函数归类的好处是显而易见的,代码变得易读且富有条理,模块变得易于理解和维护。
另外,当模块愈加庞大时,由方法字面量所构成的对象能够很容易地被拆解并重新排列。举例来说,假设你负责维护一个控制家中照明、电视、音乐和车库门API的智能家居模块,当有新设备接入进来时,如果家居模块的API组织采用了方法字面量,那么将整个模块拆解为独立的文件或子模块会变得非常简单。
警告: 尽量不要使用Function()构造函数进行函数声明,这等于做了一次隐式的eval()调用,从而给程序带来性能损耗与安全隐患等问题,更多内容参见附录A。
命名函数表达式
如你所见,以上每一种函数声明方法都有其不足之处。不过有一种函数声明既可以让代码易于组织,又能解决调用栈被匿名函数污染的问题,同时还可以在条件语句中使用。来看看灯泡API的另外一种声明方式:
var lightbulbAPI = {
toggle: function toggle() {},
getState: function getState() {},
off: function off() {},
on: function on() {},
blink: function blink() {}
};
命名函数表达式是一种具有名称的特殊匿名函数,它的名称不仅可以从函数内部获取(例如递归),还可以在调试时,显示在调用栈中。
与匿名函数一样,方法字面量仅仅只是命名函数表达式存在的一种形式,你可以在程序的任意处通过对变量赋值来使用命名函数表达式。命名函数表达式与函数声明的区别在于,命名函数表达式的函数名称仅能在函数内部被访问。在函数体之外,你仍然只能通过被函数赋值的变量或形参来获得函数引用。
test('Named function expressions.', function () {
var a = function x () {
ok(x, 'x() is usable inside the function.');
};
a();
try {
x(); // Error
} catch (e) {
ok(true, 'x() is undefined outside the function.');
}
});
警告: IE8会将命名函数表达式解析为函数声明,所以在同一作用域内,命名函数表达式会与其他变量或者函数存在同名冲突。这个问题已经在IE9中修复,而且没有在市面上其他浏览器中出现过。
这个问题其实很容易规避,只要你为命名函数表达式与被赋值变量使用相同的名称,再将其声明语句放置在函数体顶部即可。
test('Function Scope', function () {
var testDeclaration = false,
foo;
// This function gets erroneously overridden in IE8.
function bar(arg1, bleed) {
if (bleed) {
ok(false,
'Declaration bar() should NOT be callable from'
+ ' inside the expression.');
} else {
ok(true,
'Declaration bar() should be called outside the'
+ ' expression.');
}
testDeclaration = true;
}
foo = function bar(declaration, recurse) {
if (recurse) {
ok(true,
'Expression bar() should support scope safe'
+ ' recursion');
} else if (declaration === true) {
ok(true,
'Expression bar() should be callable via foo()');
bar(false, true);
} else {
// Fails in IE8 and older
ok(false,
'Expression bar() should NOT be callable outside'
+ ' the expression');
}
};
bar();
foo(true);
// Fails in IE8 and older
ok(testDeclaration,
'The bar() declaration should NOT get overridden by'
+ ' the expression bar()');
});