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的活动对象。