JavaScript函数其三:分组中的函数表达式

函数创建后的调用中用圆括号来包住它
服务器君一共花费了214.205 ms进行了6次数据库查询,努力地为您提供了这个页面。
试试阅读模式?希望听取您的建议

让我们回头并回答在文章开头提到的问题——”为何在函数创建后的立即调用中必须用圆括号来包围它?”,答案就是:表达式句子的限制就是这样的。

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

AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10

在SpiderMonkey 中,同样的情形我们完全可以在命名FE的特定对象中看到。这个特定的对象(按照标准)是普通对象——”就像表达式new Object()“,相应地,它应该从Object.prototype 继承属性,这恰恰是我们在SpiderMonkey (1.7以上的版本)看到的执行。其余的执行(包括新的TraceMonkey)不会为特定的对象设置一个原型。

function foo() {
  var x = 10;
  (function bar() {
    alert(x); // 20, 不上10,不是从foo的活动对象上得到的
    // "x"从链上查找:
    // AO(bar) - no -> __specialObject(bar) -> no
    // __specialObject(bar).[[Prototype]] - yes: 20
  })();
}
 
Object.prototype.x = 20;
foo();

NFE与Jscript

当前IE浏览器(直到JScript 5.8 — IE8)中内置的JScript 执行有很多与函数表达式(NFE)相关的bug。所有的这些bug都完全与ECMA-262-3标准矛盾;有些可能会导致严重的错误。

首先,这个例子中JScript 破坏了FE的主要规则,它不应该通过函数名存储在变量对象中。可选的FE名称应该存储在特定的对象中,并只能在函数自身(而不是别的地方)中访问。但IE直接将它存储在父变量对象中。此外,命名的FE在JScript 中作为函数声明(FD)对待。即创建于进入上下文的阶段,在源代码中的定义之前可以访问。

// FE 在变量对象里可见
testNFE();
 
(function testNFE() {
  alert('testNFE');
});
 
// FE 在定义结束以后也可见
// 就像函数声明一样
testNFE();

正如我们所见,它完全违背了规则。

其次,在声明中将命名FE赋给一个变量时,JScript 创建了两个不同的函数对象。逻辑上(特别注意的是在NFE的外部它的名称根本不应该被访问)很难命名这种行为。

var foo = function bar() {
  alert('foo');
};
 
alert(typeof bar); // "function", 
 
// 有趣的是
alert(foo === bar); // false!
 
foo.x = 10;
alert(bar.x); // 未定义
 
// 但执行的时候结果一样
 
foo(); // "foo"
bar(); // "foo"

再次看到,已经乱成一片了。但是,需要注意的是,如果与变量赋值分开,单独描述NFE(如通过组运算符),然后将它赋给一个变量,并检查其相等性,结果为true,就好像是一个对象。

(function bar() {});
 
var foo = bar;
alert(foo === bar); // true
 
foo.x = 10;
alert(bar.x); // 10

此时是可以解释的。实际上,再次创建两个对象,但那样做事实上仍保持一个。如果我们再次认为这里的NFE被作为FD对待,然后在进入上下文阶段创建FD bar。此后,在代码执行阶段第二个对象——函数表达式(FE)bar 被创建,它不会被存储。相应地,没有FE bar的任何引用,它被移除了。这样就只有一个对象——FD bar,对它的引用赋给了变量foo。

第三,就通过arguments.callee间接引用一个函数而言,它引用的是被激活的那个对象的名称(确切的说——再这里有两个函数对象。

var foo = function bar() {
  alert([
    arguments.callee === foo,
    arguments.callee === bar
  ]);
};
 
foo(); // [true, false]
bar(); // [false, true]

第四,JScript 像对待普通的FD一样对待NFE,他不服从条件表达式规则。即,就像一个FD,NFE在进入上下文时创建,在代码中最后的定义被使用。

var foo = function bar() {
  alert(1);
};
 
if (false) {
  foo = function bar() {
    alert(2);
  };
}
bar(); // 2
foo(); // 1

这种行为从”逻辑上”也可以解释。在进入上下文阶段,最后遇到的FD bar被创建,即包含alert(2)的函数。此后,在代码执行阶段,新的函数——FE bar创建,对它的引用赋给了变量foo。这样foo激活产生alert(1)。逻辑很清楚,但考虑到IE的bug,既然执行明显被破坏,并依赖于JScript 的bug,我给单词”逻辑上(logically)”加上了引号。

JScript 的第五个bug与全局对象的属性创建相关,全局对象由赋值给一个未限定的标识符(即,没有var关键字)来生成。既然NFE在这被作为FD对待,相应地,它存储在变量对象中,赋给一个未限定的标识符(即不是赋给变量而是全局对象的普通属性),万一函数的名称与未限定的标识符相同,这样该属性就不是全局的了。

(function () {
  // 不用var的话,就不是当前上下文的一个变量了
  // 而是全局对象的一个属性
  foo = function foo() {};
})();
 
//  但,在匿名函数的外部,foo这个名字是不可用的
alert(typeof foo); // 未定义

“逻辑”已经很清楚了:在进入上下文阶段,函数声明foo取得了匿名函数局部上下文的活动对象。在代码执行阶段,名称foo在AO中已经存在,即,它被作为局部变量。相应地,在赋值操作中,只是简单的更新已存在于AO中的属性foo,而不是按照ECMA-262-3的逻辑创建全局对象的新属性。

延伸阅读

此文章所在专题列表如下:

  1. 我们应该如何去了解JavaScript引擎的工作原理
  2. JavaScript探秘:编写可维护的代码的重要性
  3. JavaScript探秘:谨慎使用全局变量
  4. JavaScript探秘:var预解析与副作用
  5. JavaScript探秘:for循环(for Loops)
  6. JavaScript探秘:for-in循环(for-in Loops)
  7. JavaScript探秘:Prototypes强大过头了
  8. JavaScript探秘:eval()是“魔鬼”
  9. JavaScript探秘:用parseInt()进行数值转换
  10. JavaScript探秘:基本编码规范
  11. JavaScript探秘:函数声明与函数表达式
  12. JavaScript探秘:命名函数表达式
  13. JavaScript探秘:调试器中的函数名
  14. JavaScript探秘:JScript的Bug
  15. JavaScript探秘:JScript的内存管理
  16. JavaScript探秘:SpiderMonkey的怪癖
  17. JavaScript探秘:命名函数表达式替代方案
  18. JavaScript探秘:对象Object
  19. JavaScript探秘:原型链 Prototype chain
  20. JavaScript探秘:构造函数 Constructor
  21. JavaScript探秘:可执行的上下文堆栈
  22. 执行上下文其一:变量对象与活动对象
  23. 执行上下文其二:作用域链 Scope Chains
  24. 执行上下文其三:闭包 Closures
  25. 执行上下文其四:This指针
  26. JavaScript探秘:强大的原型和原型链
  27. JavaScript函数其一:函数声明
  28. JavaScript函数其二:函数表达式
  29. JavaScript函数其三:分组中的函数表达式
  30. JavaScript函数其四:函数构造器
  31. JavaScript变量对象其一:VO的声明
  32. JavaScript变量对象其二:VO在不同的执行上下文中
  33. JavaScript变量对象其三:执行上下文的两个阶段
  34. JavaScript变量对象其四:关于变量
  35. JavaScript变量对象其五:__parent__ 属性
  36. JavaScript作用域链其一:作用域链定义
  37. JavaScript作用域链其二:函数的生命周期
  38. JavaScript作用域链其三:作用域链特征
  39. JavaScript闭包其一:闭包概论
  40. JavaScript闭包其二:闭包的实现
  41. JavaScript闭包其三:闭包的用法

本文地址:http://www.nowamagic.net/librarys/veda/detail/1664,欢迎访问原出处。

不打个分吗?

转载随意,但请带上本文地址:

http://www.nowamagic.net/librarys/veda/detail/1664

如果你认为这篇文章值得更多人阅读,欢迎使用下面的分享功能。
小提示:您可以按快捷键 Ctrl + D,或点此 加入收藏

阅读一百本计算机著作吧,少年

很多人觉得自己技术进步很慢,学习效率低,我觉得一个重要原因是看的书少了。多少是多呢?起码得看3、4、5、6米吧。给个具体的数量,那就100本书吧。很多人知识结构不好而且不系统,因为在特定领域有一个足够量的知识量+足够良好的知识结构,系统化以后就足以应对大量未曾遇到过的问题。

奉劝自学者:构建特定领域的知识结构体系的路径中再也没有比学习该专业的专业课程更好的了。如果我的知识结构体系足以囊括面试官的大部分甚至吞并他的知识结构体系的话,读到他言语中的一个词我们就已经知道他要表达什么,我们可以让他坐“上位”毕竟他是面试官,但是在知识结构体系以及心理上我们就居高临下。

所以,阅读一百本计算机著作吧,少年!

《深入理解计算机系统(原书第2版)》 布莱恩特(Randal E.Bryant) (作者), 奥哈拉伦(David R.O'Hallaron) (作者), 龚奕利 (译者), 雷迎春 (译者)

《深入理解计算机系统》从程序员的视角详细阐述计算机系统的本质概念,并展示这些概念如何实实在在地影响应用程序的正确性、性能和实用性。全书共12章,主要内容包括信息的表示和处理、程序的机器级表示、处理器体系结构、优化程序性能、存储器层次结构、链接、异常控制流、虚拟存储器、系统级I/O、网络编程、并发编程等。书中提供子大量的例子和练习题,并给出部分答案,有助于读者加深对正文所述概念和知识的理解。

更多计算机宝库...