背景知识
JavaScript的函数具有两个主要特性,正是这两个特性让它们与众不同。第一个特性是,函数是一等对象(first-class object),第二个是函数提供作用域支持。
函数是对象,那么:
- 可以在程序执行时动态创建函数
- 可以将函数赋值给变量,可以将函数的引用拷贝至另一个变量,可以扩充函数,除了某些特殊场景外均可被删除
- 可以将函数作为参数传入另一个函数,也可以被当作返回值返回
- 函数可以包含自己的属性和方法
有可能会有这样的情况:一个函数A,它也是一个对象,拥有属性和方法,其中某个属性是另一个函数B,B可以接受函数作为参数,假设这个函数参数为C,当执行B的时候,返回另一个函数D。乍一看这里有一大堆相互关联的函数,但当你开始习惯函数的许多用法时,你会惊叹原来函数是如此灵活、强大且富有表现力。通常说来,一说到JavaScript的函数,我们首先认为它是一个对象,具有一个可以“执行”的特性,也就是说我们可以“调用”这个函数。
我们通过new Function()
构造函数来创建一个函数,这时可以明显看出函数是对象:
// 反模式,仅用于演示
var add = new Function('a, b', 'return a + b');
add(1, 2); // 返回 3
在这段代码中,毫无疑问add()
是一个对象,因为它是由构造函数创建的。这里并不推荐使用Function()
构造函数来创建函数(和eval()
一样糟糕),因为程序逻辑代码是以字符串的形式传入构造器的。这样的代码可读性差,写起来也很费劲,你还要对代码中的引号做转义处理,并需要特别关注为了保持可读性而保留的空格和缩进。
函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let
来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var
声明的变量都是局部变量,在函数外部是不可见的。刚才所说的花括号无法提供作用域支持的意思是说,如果在if
条件句、for
或while
循环体内用var
定义了变量,这个变量并不是属于if
语句或for
(while
)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量作用域的最佳选择。
术语
首先我们先简单讨论下与创建函数相关的术语,因为精确无歧义的术语约定非常重要。
看下这个代码片段:
// 具名函数表达式
var add = function add(a, b) {
return a + b;
};
这段代码描述了一个函数,这种描述称为“具名函数表达式”。
如果省略掉函数表达式中的名字(比如下面的示例代码),这时它是“匿名函数表达式”,通常我们称之为“匿名函数”,比如:
// 匿名函数表达式,又称匿名函数
var add = function (a, b) {
return a + b;
};
因此“函数表达式”是一个更广义的概念,“具名函数表达式”是函数表达式的一种特殊形式,仅仅当需要给函数定义一个可选的名字时使用。
当省略第二个add
,它就成了没有名字的函数表达式,这不会对函数定义和调用语法造成任何影响。带名字和不带名字唯一的区别是函数对象的name
属性是否为空字符串。name
属性属于语言的扩展(未在ECMA标准中定义),但很多环境都实现了。如果不省略第二个add
,那么add.name
是”add”,name
属性在用像Firebug之类的调试工具进行调试的过程中非常有用,它也可以让函数递归调用自身,如果是其他情况,则可以省略它。
最后来看一下“函数声明”,函数声明的语法和其他语言中的语法非常类似:
function foo() {
// 函数体
}
从语法上来看,具名函数表达式和函数声明非常像,特别是当不需要将函数表达式赋值给一个变量的时候(在本章后面所讲到的回调模式中有类似的例子)。多数情况下,函数声明和具名函数表达式在外观上没有多少不同,只是它们在函数执行时对上下文的影响有所区别,下一小节会讲到。
两种语法的一个区别是末尾的分号。函数声明末尾不需要分号,而函数表达式末尾是需要分号的。推荐你始终不要丢掉函数表达式末尾的分号,即便JavaScript可以进行分号补全,也不要冒险这样做。
另外我们经常看到“函数字面量”。它用来表示函数表达式或具名函数表达式。由于这个术语是有歧义的,所以最好不要用它。
声明 vs 表达式:命名与提前
那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,就只能使用函数表达式了,将函数作为参数传递、在对象字面量中定义方法都是这样的例子:
// 作为参数传递给callMe的函数表达式
callMe(function () {
// 我是匿名函数表达式,也叫匿名函数
});
// 这是一个具名函数表达式
callMe(function me() {
// 我是具名函数表达式,我的名字是“me”
});
// 另一个函数表达式
var myobject = {
say: function () {
// 我是函数表达式
}
};
函数声明只能出现在“程序代码”中,也就是说在别的函数体内或在全局。这个定义不能赋值给变量或属性,同样不能作为函数调用的参数。(译注:注意这里说的是函数声明的语句,而不是通过声明语句定义出来的函数本身。任何函数都是可以被赋值给变量和属性的,也可以被作为参数传递。)下面这个例子是函数声明的合法用法,这里所有的函数foo()
,bar()
和local()
都使用函数声明来定义:
// 全局作用域
function foo() {}
function local() {
// 本地作用域
function bar() {}
return bar;
}
函数的name属性
选择用哪种模式定义函数时的另一个考虑是只读属性name
的可用性。尽管标准规范中并未定义,但很多运行环境都实现了name
属性,在函数声明和具名函数表达式中是有name
属性的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。
function foo() {} // 函数声明
var bar = function () {}; // 匿名函数表达式
var baz = function baz() {}; // 具名函数表达式
foo.name; // "foo"
bar.name; // ""
baz.name; // "baz"
在Firebug或其他工具中调试程序时name
属性非常有用,它可以用来显示当前正在执行的函数。同样可以通过name
属性来递归地调用函数自身。如果你对这些场景不感兴趣,那么请尽可能地使用匿名函数表达式,这样会更简单、且冗余代码更少。
相对函数声明而言,函数表达式的语法更能说明函数是一种和其它对象类似的对象,而不是语言中某种特别的组成部分。
我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:
var foo = function bar(){};
。然而,这种用法的行为在浏览器中的兼容性不好(特别是IE中),因此并不推荐大家使用这种模式。
声明提前
通过前面的讲解,你可能以为函数声明和具名函数表达式是完全等价的。事实上并不是这样,主要区别在于“声明提前”的行为。
术语“提前”并未在ECMAScript中定义,但是它是一种很好地描述这种行为的方法。
我们知道,不管在函数内何处声明变量,变量都会自动提前至函数体的顶部。对于函数来说亦是如此,因为他们也是一种对象,赋值给了变量。需要注意的是,函数声明定义的函数不仅能让声明提前,还能让定义提前,看一下这段示例代码:
// 反模式,仅用于演示
// 全局函数
function foo() {
alert('global foo');
}
function bar() {
alert('global bar');
}
function hoistMe() {
console.log(typeof foo); // "function"
console.log(typeof bar); // "undefined"
foo(); // "local foo"
bar(); // TypeError: bar is not a function
// 函数声明:
// 变量foo和它的定义实现都被提前了
function foo() {
alert('local foo');
}
// 函数表达式:
// 只有变量bar被提前,它的定义实现没有被提前
var bar = function () {
alert('local bar');
};
}
hoistMe();
在这段代码中,和普通的变量一样,hoistMe()
函数中的foo
和bar
被“搬运”到了顶部,覆盖了全局的foo()
和bar()
。不同之处在于,本地的foo()
的位置并不在前面,但它的定义却被提前到了顶部并能正常工作,而bar()
的定义并未提前,只有声明提前了。因此当程序执行到bar()
定义的位置之前,它的值都不是函数,而是undefined
(在此期间全局的bar()
都是被本地覆盖的)。
到目前为止我们介绍了必要的背景知识和函数定义相关的术语,下面开始介绍一些JavaScript所提供的函数相关的模式,我们从回调模式开始。再次强调JavaScript函数的两个特性,掌握这两点至关重要:
- 函数是对象
- 函数提供本地变量作用域