前言
本文翻译自Functions
概述
本文将介绍ECMAScript中非常普遍的对象——函数。我们将着重介绍几种不同类型的函数是怎么样影响上下文变量对象的,以及每种类型的函数的作用域中都包含什么,并回答诸如下面这样的问题:下面声明的函数有什么区别吗?(如果有,区别是什么)。
var foo = function () {
...
};
上述方式创建的函数和如下方式创建的有什么不同?
function foo() {
...
}
下面代码为什么要用一个括号包起来呢?
(function () {
...
})();
函数类型
ECMAScript中包含三类函数,每一类都有各自的特点
函数声明(Function Declaration)
函数声明(FD)是指这样的函数
* 一个必选的函数名
* 代码位置:要么是程序级别,要么在另一个函数体中
* 在进入上下文阶段时被创建
* 会影响变量对象
如下声明:
function exampleFunc() {
...
}
这类函数的主要特性是:只有它们可以影响变量对象(储存在上下文的 VO 中)。这个特性同时也解释了第二个重要点(它是变量对象特性的结果)—— 在代码执行阶段它们已经可用(因为 FD 在进入上下文阶段已经存在于 VO 中 —— 代码执行之前)。
例如(函数在其声明之前被调用)
foo();
function foo() {
alert('foo');
}
从定义中提到了非常重要的一点————函数声明在代码中的位置:
// 函数声明可以直接在程序级别的全局上下文中
function globalFD() {
// 或者直接在另外一个函数的函数体中
function innerFD() {}
}
只有这两个位置可以声明函数,也就是说,在表达式的位置或者是代码块中进行函数声明都是不可以的
函数表达式(Function Expression)
函数表达式(简称:FE)是指这样的函数:
* 代码位置必须要在表达式的位置
* 函数名是可选的
* 不会影响变量对象
* 在执行代码阶段才被创建
这类函数的主要特性是:它们的代码总是在表达式的位置。最简单的表达式的例子就是赋值表达式:
var foo = function () {
...
};
上述例子中将一个匿名函数赋值给了变量 foo,之后该函数就可以通过 foo 来访问了 —— foo()
正如定义中提到的,函数表达式也可以有名字:
var foo = function _foo() {
...
};
这里要注意的是,在函数表达式的外部可以通过变量foo——foo() 来访问,而在函数内部(比如递归调用),还可以用_foo
(译者注:但在外部是无法使用_foo
的)。
当函数表达式有名字的时候,它很难和函数声明作区分。不过,如果仔细看这两者的定义的话,要区分它们还是很容易的:函数表达式总是在表达式的位置。 如下例子展示的各类 ECMAScript 表达式都属于函数表达式
// 在括号中(grouping operator)只可能是表达式
(function foo() {});
// 在数组初始化中 —— 同样也只能是表达式
[function bar() {}];
// 逗号操作符也只能跟表达式
1, function baz() {};
定义中还提到函数表达式是在执行代码阶段创建的,并且不是存储在变量对象上的。如下所示:
// 不论是在定义前还是定义后,FE都是无法访问的
// (因为它是在代码执行阶段创建出来的),
alert(foo); // "foo" is not defined
(function foo() {});
// 后面也没用,因为它根本就不在VO中
alert(foo); // "foo" is not defined
问题来了,函数表达式要来干嘛?其实答案是很明显的 —— 在表达式中使用,从而避免对变量对象造成“污染”。最简单的例子就是将函数作为参数传递给另外一个函数:
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function baz() {
alert('foo.baz');
});
上述例子中,部分变量存储了对FE的引用,这样函数就会保留在内存中并在之后,可以通过变量来访问(因为变量是可以影响 VO 的):
另外一个例子是创建封装的闭包从外部上下文中隐藏辅助性数据(在下面的例子中我们使用 FE,它在创建后立即调用):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
alert(x);
};
})();
foo.bar(); // 10;
alert(x); // "x" 未定义
我们看到函数 foo.bar(通过其[[Scope]]属性)获得了对函数 initialize 内部变量 x 的访问。 而同样的 x 在外部就无法访问到。很多库都使用这种策略来创建“私有”数据以及隐藏辅助数据。通常,这样的情况下 FE 的名字都会省略掉:
“有关括号”的问题
现在让我们来回答本文开始提到的问题——为什么立即执行的函数需要用括号将其包起来?”答案就是:将函数限制为表达式语句
标准中提到,表达式语句(ExpressionStatement)不能以左大括号 { 开始 —— 因为这样一来就和代码块冲突了, 也不能以function
关键字开始,因为这样一来又和函数声明冲突了。也就是说,以如下所示的方式来定义一个立即执行的函数,解释器都会抛出错误,只是原因不同:
function () {
...
}();
// or with a name
function foo() {
...
}();
如果我们是在全局代码(程序级别)中这样定义函数,解释器会以函数声明来处理,因为它看到了是以 function 开始的。在第一个例子中,会抛出语法错误,原因是既然是个函数声明,则缺少函数名了(一个函数声明其名字是必须的)。
而在第二个例子中,看上去已经有了名字了(foo),应该会正确执行。然而,这里还是会抛出语法错误——分组操作符内部缺少表达式。这里要注意的是,这个例子中,函数声明后面的()会被当组操作符来处理,而非函数调用的()。因此,如果我们有如下代码:
// "foo" 是函数声明
// 并且是在进入上下文的时候创建的
alert(foo); // function
function foo(x) {
alert(x);
}(1); // 这里只是组操作符,并非调用!
foo(10); // 这里就是调用了, 10
上述代码其实就是如下代码:
// function declaration
function foo(x) {
alert(x);
}
// 含表达式的组操作符
(1);
// 另外一个组操作符
// 包含一个函数表达式
(function () {});
// 这里面也是表达式
("foo");
如果我们定义一个如下代码(定义里包含一个语句),我们可能会说,定义歧义,会得到报错:
if (true) function foo() {alert(1)}
根据规范,上述代码是错误的(一个表达式语句不能以function关键字开头)。然而,正如我们在后面要看到的,没有一种实现对其抛出错误, 它们各自按照自己的方式在处理。
那么究竟怎样才能创建一个立即执行的函数呢?答案很明显,它必须是个函数表达式,而不能是函数声明。而创建表达式最简单的方式就是使用上述提到的组操作符。因为在组操作符中只可能是表达式。 这样一来解释器也不会纠结了,会果断将其以 FE 的方式来处理。这样的函数将在执行阶段创建出来,然后立马执行,随后被移除(如果有没有对其的引用的话):
(function foo(x) {
alert(x);
})(1); // 好了,这样就是函数调用了,而不再是组操作符了,1
要注意的是,在下面的例子中,函数调用,其括号就不再是必须的了,因为函数本来就在表达式的位置了,解释器自然会以 FE 来处理,并且会在执行代码阶段创建该函数:
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
因此,对“括号有关”问题的完整的回答则如下所示:
如果要在函数创建后立马进行函数调用,并且函数不在表达式的位置时,括号就是必须的 —— 这样情况下,其实是手动的将其转换成了 FE。 而当解释器直接将其以 FE 的方式处理的时候,说明 FE 本身就在函数表达式的位置 —— 这个时候括号就不是必须的了
另外,除了使用括号的方式将函数转换成为FE之外,还有其他的方式,如下所示:
1, function () {
alert('anonymous function is called');
}();
// 或者这样
!function () {
alert('ECMAScript');
}();
// 当然,还有其他很多方式
不过,括号是最通用也是最优雅的方式。
顺便提下,组操作符既可以包含没有调用括号的函数,又可以包含有调用括号的函数,这两者都是合法的 FE:
(function () {})();
(function () {}());
实现扩展:函数语句
看如下代码,符合规范的解释器都无法解释这样的代码:
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 还是 0 ? 在不同引擎中测试
这里有必要提下:根据标准,上述代码结构是不合法的,因为,此前我们就介绍过,函数声明是不能出现在代码块中的(这里 if 和 else 就包含代码块)。此前提到的,函数声明只能出现在两个位置:程序级别或者另外一个函数的函数体中。
为什么这种结构是错误的呢?因为在代码块中只允许语句。函数要想在这个位置出现的唯一可能就是要成为表达式语句。 但是,根据定义表达式语句又不能以左大括号开始(这样会与代码块冲突)也不能以 function 关键字开始(这样又会和 FD 冲突)。
然而,在错误处理部分,规范允许实现对程序语法进行扩展。而上述例子就是其中一种扩展。目前,所有的实现中都不会对上述情况抛出错误,都会以各自的方式进行处理。
因此根据规范,上述 if-else 中应当需要 FE。然而,绝大多数实现中都在进入上下文的时候在这里简单地创建了 FD,并且使用了最后一次的声明。最后 foo 函数显示了 1,尽管理论上 else 中的代码根本不会被执行到。
而 SpiderMonkey(TraceMonkey 也是)实现中,会将上述情况以两种方式来处理:一方面它不会将这样的函数以函数声明来处理(也就意味着函数会在执行代码阶段才会创建出来),然而,另外一方面,它们又不属于真正的函数表达式,因为在没有括号的情况是不能作函数调用的(同样会有解析错误 —— 和 FD 冲突),它们还是存储在变量对象中。
我认为 SpiderMonkey 单独引入了自己的中间函数类型 ——(FE+FD),这样的做法是正确的。这样的函数会根据时间和对应的条件正确创建出来,不像 FE。和 FD 有点类似,可以在外部对其进行访问。SpiderMonkey 将这种语法扩展命名为函数语句(Function Statement)(简称 FS)