js高端系列教程(29)——JavaScript函数其三:分组中的函数表达式
- 格式:docx
- 大小:28.93 KB
- 文档页数:9
让我们回头并回答在文章开头提到的问题——”为何在函数创建后的立即调用中必须用圆括号来包围它?”,答案就是:表达式句子的限制就是这样的。
(function () {
...
})();
按照标准,表达式语句不能以一个大括号{开始是因为他很难与代码块区分,同样,他也不能以函数关键字开始,因为很难与函数声明进行区分。即,所以,如果我们定义一个立即执行的函数,在其创建后立即按以下方式调用:
function () {
...
}();
// 即便有名称
function foo() {
...
}();
我们使用了函数声明,上述2个定义,解释器在解释的时候都会报错,但是可能有多种原因。
如果在全局代码里定义(也就是程序级别),解释器会将它看做是函数声明,因为他是以function关键字开头,第一个例子,我们会得到SyntaxError错误,是因为函数声明没有名字(我们前面提到了函数声明必须有名字)。
第二个例子,我们有一个名称为foo的一个函数声明正常创建,但是我们依然得到了一个语法错误——没有任何表达式的分组操作符错误。在函数声明后面他确实是一个分组操作符,而不是一个函数调用所使用的圆括号。所以如果我们声明如下代码:
// "foo" 是一个函数声明,在进入上下文的时候创建
alert(foo); // 函数
function foo(x) {
alert(x);
}(1); // 这只是一个分组操作符,不是函数调用!
foo(10); // 这才是一个真正的函数调用,结果是10
上述代码是没有问题的,因为声明的时候产生了2个对象:一个函数声明,一个带有1的分组操作,上面的例子可以理解为如下代码:
// 函数声明
function foo(x) {
alert(x); }
// 一个分组操作符,包含一个表达式1
(1);
// 另外一个操作符,包含一个function表达式
(function () {});
// 这个操作符里,包含的也是一个表达式"foo"
("foo");
// 等等
如果我们定义一个如下代码(定义里包含一个语句),我们可能会说,定义歧义,会得到报错:
if (true) function foo() {alert(1)}
根据规范,上述代码是错误的(一个表达式语句不能以function关键字开头),但下面的例子就没有报错,想想为什么?
我们如果来告诉解释器:我就像在函数声明之后立即调用,答案是很明确的,你得声明函数表达式function expression,而不是函数声明function declaration,并且创建表达式最简单的方式就是用分组操作符括号,里边放入的永远是表达式,所以解释器在解释的时候就不会出现歧义。在代码执行阶段这个的function就会被创建,并且立即执行,然后自动销毁(如果没有引用的话)。
(function foo(x) {
alert(x);
})(1); // 这才是调用,不是分组操作符
上述代码就是我们所说的在用括号括住一个表达式,然后通过(1)去调用。
注意,下面一个立即执行的函数,周围的括号不是必须的,因为函数已经处在表达式的位置,解析器知道它处理的是在函数执行阶段应该被创建的FE,这样在函数创建后立即调用了函数。
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
就像我们看到的,foo.bar是一个字符串而不是一个函数,这里的函数仅仅用来根据条件参数初始化这个属性——它创建后并立即调用。
因此,”关于圆括号”问题完整的答案如下:当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成FE。如果解析器知道它处理的是FE,就没必要用圆括号。
除了大括号以外,如下形式也可以将函数转化为FE类型,例如:
// 注意是1,后面的声明
1, function () {
alert('anonymous function is called');
}();
// 或者这个
!function () {
alert('ECMAScript');
}();
// 其它手工转化的形式
...
但是,在这个例子中,圆括号是最简洁的方式。
顺便提一句,组表达式包围函数描述可以没有调用圆括号,也可包含调用圆括号,即,下面的两个表达式都是正确的FE。
下面的代码,根据任何一个function声明都不应该被执行:
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 or 0 ?实际在上不同环境下测试得出个结果不一样 这里有必要说明的是,按照标准,这种句法结构通常是不正确的,因为我们还记得,一个函数声明(FD)不能出现在代码块中(这里if和else包含代码块)。我们曾经讲过,FD仅出现在两个位置:程序级(Program level)或直接位于其它函数体中。
因为代码块仅包含语句,所以这是不正确的。可以出现在块中的函数的唯一位置是这些语句中的一个——上面已经讨论过的表达式语句。但是,按照定义它不能以大括号开始(既然它有别于代码块)或以一个函数关键字开始(既然它有别于FD)。
但是,在标准的错误处理章节中,它允许程序语法的扩展执行。这样的扩展之一就是我们见到的出现在代码块中的函数。在这个例子中,现今的所有存在的执行都不会抛出异常,都会处理它。但是它们都有自己的方式。
if-else分支语句的出现意味着一个动态的选择。即,从逻辑上来说,它应该是在代码执行阶段动态创建的函数表达式(FE)。但是,大多数执行在进入上下文阶段时简单的创建函数声明(FD),并使用最后声明的函数。即,函数foo将显示”1″,事实上else分支将永远不会执行。
但是,SpiderMonkey (和TraceMonkey)以两种方式对待这种情况:一方面它不会将函数作为声明处理(即,函数在代码执行阶段根据条件创建),但另一方面,既然没有括号包围(再次出现解析错误——”与FD有别”),他们不能被调用,所以也不是真正的函数表达式,它储存在变量对象中。
我个人认为这个例子中SpiderMonkey 的行为是正确的,拆分了它自身的函数中间类型——(FE+FD)。这些函数在合适的时间创建,根据条件,也不像FE,倒像一个可以从外部调用的FD,SpiderMonkey将这种语法扩展 称之为函数语句(缩写为FS);该语法在MDC中提及过。
命名函数表达式的特性
当函数表达式FE有一个名称(称为命名函数表达式,缩写为NFE)时,将会出现一个重要的特点。从定义(正如我们从上面示例中看到的那样)中我们知道函数表达式不会影响一个上下文的变量对象(那样意味着既不可能通过名称在函数声明之前调用它,也不可能在声明之后调用它)。但是,FE在递归调用中可以通过名称调用自身。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" 是可用的 })();
// 在外部,是不可用的
foo(); // "foo" 未定义
“foo”储存在什么地方?在foo的活动对象中?不是,因为在foo中没有定义任何”foo”。在上下文的父变量对象中创建foo?也不是,因为按照定义——FE不会影响VO(变量对象)——从外部调用foo我们可以实实在在的看到。那么在哪里呢?
以下是关键点。当解释器在代码执行阶段遇到命名的FE时,在FE创建之前,它创建了辅助的特定对象,并添加到当前作用域链的最前端。然后它创建了FE,此时(正如我们在第四章 作用域链知道的那样)函数获取了[[Scope]] 属性——创建这个函数上下文的作用域链)。此后,FE的名称添加到特定对象上作为唯一的属性;这个属性的值是引用到FE上。最后一步是从父作用域链中移除那个特定的对象。让我们在伪码中看看这个算法:
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // 从作用域链中删除定义的特殊对象specialObject
因此,在函数外部这个名称不可用的(因为它不在父作用域链中),但是,特定对象已经存储在函数的[[scope]]中,在那里名称是可用的。
但是需要注意的是一些实现(如Rhino)不是在特定对象中而是在FE的激活对象中存储这个可选的名称。Microsoft 中的执行完全打破了FE规则,它在父变量对象中保持了这个名称,这样函数在外部变得可以访问。
NFE 与SpiderMonkey
我们来看看NFE和SpiderMonkey的区别,SpiderMonkey 的一些版本有一个与特定对象相关的属性,它可以作为bug来对待(虽然按照标准所有的都那样实现了,但更像一个ECMAScript标准上的bug)。它与标识符的解析机制相关:作用域链的分析是二维的,在标识符的解析中,同样考虑到作用域链中每个对象的原型链。 如果我们在Object.prototype中定义一个属性,并引用一个”不存在(nonexistent)”的变量。我们就能看到这种执行机制。这样,在下面示例的”x”解析中,我们将到达全局对象,但是没发现”x”。但是,在SpiderMonkey 中全局对象继承了Object.prototype中的属性,相应地,”x”也能被解析。
Object.prototype.x = 10;
(function () {
alert(x); // 10
})();
活动对象没有原型。按照同样的起始条件,在上面的例子中,不可能看到内部函数的这种行为。如果定义一个局部变量”x”,并定义内部函数(FD或匿名的FE),然后再内部函数中引用”x”。那么这个变量将在父函数上下文(即,应该在哪里被解析)中而不是在Object.prototype中被解析。
Object.prototype.x = 10;
function foo() {
var x = 20;
// 函数声明
function bar() {
alert(x);
}
bar(); // 20, 从foo的变量对象AO中查询
// 匿名函数表达式也是一样
(function () {
alert(x); // 20, 也是从foo的变量对象AO中查询
})();
}
foo();
尽管如此,一些执行会出现例外,它给活动对象设置了一个原型。因此,在Blackberry 的执行中,上面例子中的”x”被解析为”10”。也就是说,既然在Object.prototype中已经找到了foo的值,那么它就不会到达foo的活动对象。