本节书摘来华章计算机出版社《JavaScript应用程序设计》一书中的第2章,第2.2节,作者:Eric Elliott 更多章节内容可以访问云栖社区“异步社区”公众号查看。
在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()'); }); 相关资源:JavaScript高级程序设计(附源码)