背景知识

JavaScript的函数具有两个主要特性,正是这两个特性让它们与众不同。第一个特性是,函数是一等对象(first-class object),第二个是函数提供作用域支持。

函数是对象,那么:

  • 可以在程序执行时动态创建函数
  • 可以将函数赋值给变量,可以将函数的引用拷贝至另一个变量,可以扩充函数,除了某些特殊场景外均可被删除
  • 可以将函数作为参数传入另一个函数,也可以被当作返回值返回
  • 函数可以包含自己的属性和方法

有可能会有这样的情况:一个函数A,它也是一个对象,拥有属性和方法,其中某个属性是另一个函数B,B可以接受函数作为参数,假设这个函数参数为C,当执行B的时候,返回另一个函数D。乍一看这里有一大堆相互关联的函数,但当你开始习惯函数的许多用法时,你会惊叹原来函数是如此灵活、强大且富有表现力。通常说来,一说到JavaScript的函数,我们首先认为它是一个对象,具有一个可以“执行”的特性,也就是说我们可以“调用”这个函数。

我们通过new Function()构造函数来创建一个函数,这时可以明显看出函数是对象:

  1. // 反模式,仅用于演示
  2. var add = new Function('a, b', 'return a + b');
  3. add(1, 2); // 返回 3

在这段代码中,毫无疑问add()是一个对象,因为它是由构造函数创建的。这里并不推荐使用Function()构造函数来创建函数(和eval()一样糟糕),因为程序逻辑代码是以字符串的形式传入构造器的。这样的代码可读性差,写起来也很费劲,你还要对代码中的引号做转义处理,并需要特别关注为了保持可读性而保留的空格和缩进。

函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var声明的变量都是局部变量,在函数外部是不可见的。刚才所说的花括号无法提供作用域支持的意思是说,如果在if条件句、forwhile循环体内用var定义了变量,这个变量并不是属于if语句或forwhile)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量作用域的最佳选择。

术语

首先我们先简单讨论下与创建函数相关的术语,因为精确无歧义的术语约定非常重要。

看下这个代码片段:

  1. // 具名函数表达式
  2. var add = function add(a, b) {
  3. return a + b;
  4. };

这段代码描述了一个函数,这种描述称为“具名函数表达式”。

如果省略掉函数表达式中的名字(比如下面的示例代码),这时它是“匿名函数表达式”,通常我们称之为“匿名函数”,比如:

  1. // 匿名函数表达式,又称匿名函数
  2. var add = function (a, b) {
  3. return a + b;
  4. };

因此“函数表达式”是一个更广义的概念,“具名函数表达式”是函数表达式的一种特殊形式,仅仅当需要给函数定义一个可选的名字时使用。

当省略第二个add,它就成了没有名字的函数表达式,这不会对函数定义和调用语法造成任何影响。带名字和不带名字唯一的区别是函数对象的name属性是否为空字符串。name属性属于语言的扩展(未在ECMA标准中定义),但很多环境都实现了。如果不省略第二个add,那么add.name是”add”,name属性在用像Firebug之类的调试工具进行调试的过程中非常有用,它也可以让函数递归调用自身,如果是其他情况,则可以省略它。

最后来看一下“函数声明”,函数声明的语法和其他语言中的语法非常类似:

  1. function foo() {
  2. // 函数体
  3. }

从语法上来看,具名函数表达式和函数声明非常像,特别是当不需要将函数表达式赋值给一个变量的时候(在本章后面所讲到的回调模式中有类似的例子)。多数情况下,函数声明和具名函数表达式在外观上没有多少不同,只是它们在函数执行时对上下文的影响有所区别,下一小节会讲到。

两种语法的一个区别是末尾的分号。函数声明末尾不需要分号,而函数表达式末尾是需要分号的。推荐你始终不要丢掉函数表达式末尾的分号,即便JavaScript可以进行分号补全,也不要冒险这样做。

另外我们经常看到“函数字面量”。它用来表示函数表达式或具名函数表达式。由于这个术语是有歧义的,所以最好不要用它。

声明 vs 表达式:命名与提前

那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,就只能使用函数表达式了,将函数作为参数传递、在对象字面量中定义方法都是这样的例子:

  1. // 作为参数传递给callMe的函数表达式
  2. callMe(function () {
  3. // 我是匿名函数表达式,也叫匿名函数
  4. });
  5. // 这是一个具名函数表达式
  6. callMe(function me() {
  7. // 我是具名函数表达式,我的名字是“me”
  8. });
  9. // 另一个函数表达式
  10. var myobject = {
  11. say: function () {
  12. // 我是函数表达式
  13. }
  14. };

函数声明只能出现在“程序代码”中,也就是说在别的函数体内或在全局。这个定义不能赋值给变量或属性,同样不能作为函数调用的参数。(译注:注意这里说的是函数声明的语句,而不是通过声明语句定义出来的函数本身。任何函数都是可以被赋值给变量和属性的,也可以被作为参数传递。)下面这个例子是函数声明的合法用法,这里所有的函数foo()bar()local()都使用函数声明来定义:

  1. // 全局作用域
  2. function foo() {}
  3. function local() {
  4. // 本地作用域
  5. function bar() {}
  6. return bar;
  7. }

函数的name属性

选择用哪种模式定义函数时的另一个考虑是只读属性name的可用性。尽管标准规范中并未定义,但很多运行环境都实现了name属性,在函数声明和具名函数表达式中是有name属性的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。

  1. function foo() {} // 函数声明
  2. var bar = function () {}; // 匿名函数表达式
  3. var baz = function baz() {}; // 具名函数表达式
  4. foo.name; // "foo"
  5. bar.name; // ""
  6. baz.name; // "baz"

在Firebug或其他工具中调试程序时name属性非常有用,它可以用来显示当前正在执行的函数。同样可以通过name属性来递归地调用函数自身。如果你对这些场景不感兴趣,那么请尽可能地使用匿名函数表达式,这样会更简单、且冗余代码更少。

相对函数声明而言,函数表达式的语法更能说明函数是一种和其它对象类似的对象,而不是语言中某种特别的组成部分。

我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:var foo = function bar(){};。然而,这种用法的行为在浏览器中的兼容性不好(特别是IE中),因此并不推荐大家使用这种模式。

声明提前

通过前面的讲解,你可能以为函数声明和具名函数表达式是完全等价的。事实上并不是这样,主要区别在于“声明提前”的行为。

术语“提前”并未在ECMAScript中定义,但是它是一种很好地描述这种行为的方法。

我们知道,不管在函数内何处声明变量,变量都会自动提前至函数体的顶部。对于函数来说亦是如此,因为他们也是一种对象,赋值给了变量。需要注意的是,函数声明定义的函数不仅能让声明提前,还能让定义提前,看一下这段示例代码:

  1. // 反模式,仅用于演示
  2. // 全局函数
  3. function foo() {
  4. alert('global foo');
  5. }
  6. function bar() {
  7. alert('global bar');
  8. }
  9. function hoistMe() {
  10. console.log(typeof foo); // "function"
  11. console.log(typeof bar); // "undefined"
  12. foo(); // "local foo"
  13. bar(); // TypeError: bar is not a function
  14. // 函数声明:
  15. // 变量foo和它的定义实现都被提前了
  16. function foo() {
  17. alert('local foo');
  18. }
  19. // 函数表达式:
  20. // 只有变量bar被提前,它的定义实现没有被提前
  21. var bar = function () {
  22. alert('local bar');
  23. };
  24. }
  25. hoistMe();

在这段代码中,和普通的变量一样,hoistMe()函数中的foobar被“搬运”到了顶部,覆盖了全局的foo()bar()。不同之处在于,本地的foo()的位置并不在前面,但它的定义却被提前到了顶部并能正常工作,而bar()的定义并未提前,只有声明提前了。因此当程序执行到bar()定义的位置之前,它的值都不是函数,而是undefined(在此期间全局的bar()都是被本地覆盖的)。

到目前为止我们介绍了必要的背景知识和函数定义相关的术语,下面开始介绍一些JavaScript所提供的函数相关的模式,我们从回调模式开始。再次强调JavaScript函数的两个特性,掌握这两点至关重要:

  • 函数是对象
  • 函数提供本地变量作用域