JScript在函数声明和函数表达式方面不合ECMA标准,这已经是一个老生常谈的话题了。最近[url=http://blog.csdn.net/aimingoo/archive/2008/07/22/2692512.aspx]aimingoo谈到eval问题[/url]的时候再一次触及到这个问题。
大体就是 eval('(function (){})') 不返回函数对象,但是 eval('(0, function (){})') 却返回函数对象。此外,还有 eval('(function f(){})') 会在当前执行环境中产生 f 指向一个函数(当然,按照标准是不应该有的)。
从现象上看,这表示JScript并没有把孤立的“(function (){})”作为function表达式。按照规范,(function(){})就是一个表达式,但是遗憾的是对于JScript来说,括号还不够,一定得有运算符介入或者语句expect一个表达式时,比如简单的逗号运算符、return语句之类的,才会被认为是表达式。
此外“(function f(){})”按照规范应该也被认为是一个function表达式,但是JScript的行为却好像是一个函数声明。
这里就有有趣的问题了。
0,function f1(){}
这样一个语句中,究竟是函数表达式还是函数声明呢?
答案是:两者皆是。
当然这仅是从效果上说,因为本来这样的行为就不符合标准的定义。
不过更微妙的问题是,这个表达式返回的函数对象和所声明的f1函数,其实并非同一个函数!
对此,你可以在IE浏览器的地址栏里执行:javascript:alert(eval('0,function f1(){}')==f1),结果是false。
也就是说function表达式产生了一个不同于function声明的结果——这里产生了两个不同的函数对象!
回想到我以前对于with语句行为的测试,可以确认,JScript 5.0到5.5的时候,对function声明和function表达式的行为做了很大的修订,这可能是上述奇怪表现的源头。不过我手头没有JScript 5.0,所以无法做进一步的确认。
另一个例子是:javascript:alert(eval('(function f1(){return arguments.callee==f1})()'));alert(f1())
返回结果false和true。
注意,这边的不相等,并非COM对象包装所造成的假象(比如两次读取event包装对象不相等,或者跨window/frame所造成的问题),而真的是两个函数,其实际执行效果其实也是不同的(虽然这里这个例子无法表现出来,但是如果加上with语句就会有差别了)。
产生这个用于函数表达式的函数,和函数声明所产生的函数一样,是需要CPU资源的。对此,我写了一个测试:
执行结果大体如下:
D:\>cscript test-func-benchmark.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation. All rights reserved.
2704
7593
7563
10047
7828
7735
10079
10078
这个测试从侧面再一次证明了诸如var x=function f(){}这样的语句,实际会产生两个不同的函数。
按照我的猜测,最初的JScript可能是这样处理的:任何函数结构一律按照函数声明提前处理(所以总是产生变量绑定),函数表达式则被替换为函数对象的引用。
后来JScript 5.5为了改善对于with的处理而加入了真正的函数表达式实现,本来它可以完全fix这个问题。但是为了向前兼容而保持对于具名函数一律产生变量绑定,并且也保持了其with作用域的行为(就with语句中的function声明而言,按照规范实际应可视为语法错误——不过SpiderMonkey等引擎都按照function表达式等同处理,而JScript则选择忽略with)。最后得到的就是一个语句产生两份函数对象这样微妙的结果。
进一步我们可以大胆猜想,JScript 5.5大体上是这样:JScript在解析代码时,如果函数有具名,就会产生一份函数对象绑定到对应该名字的变量上,函数声明和绑定会被提前到当前执行上下文的最前端,因此总是会忽略with语句。而如果是一个函数表达式,则到实际运行到包含函数表达式的语句时,如果这个函数表达式有被用到(如有运算符时),就会产生一份函数对象,这个过程是在运行时的,所以会纳入with语句对于scope的修改。这两个步骤是共存的,所以可能出现一个语句最终产生出两份有微妙差异的函数对象的现象(实际上并不一定是两份,因为函数表达式是每运行到该语句时就产生一个函数对象,所以是n+1份)。
最后我们来看单独一句“(function (){})”,它既不是声明语句也不是表达式(严格的说是既没有函数声明的效果,也没有函数表达式的效果),实际上这里什么也没执行,这个代码被 JScript抛弃掉了(或者说这里是一个被优化掉了的函数表达式)。这一点可以通过类似我上面的性能测试证明之(即包含这个语句的函数其执行时间与一个空函数是一样的)。
之所以单独的括号不会触发函数表达式,也许是因为括号只是用于产生AST的结构而不像运算符等会产生独立的节点,JScript没有对此作特别处理,所以单独括号和没有括号并无差别。当然这也只是我的臆测而已。
大体就是 eval('(function (){})') 不返回函数对象,但是 eval('(0, function (){})') 却返回函数对象。此外,还有 eval('(function f(){})') 会在当前执行环境中产生 f 指向一个函数(当然,按照标准是不应该有的)。
从现象上看,这表示JScript并没有把孤立的“(function (){})”作为function表达式。按照规范,(function(){})就是一个表达式,但是遗憾的是对于JScript来说,括号还不够,一定得有运算符介入或者语句expect一个表达式时,比如简单的逗号运算符、return语句之类的,才会被认为是表达式。
此外“(function f(){})”按照规范应该也被认为是一个function表达式,但是JScript的行为却好像是一个函数声明。
这里就有有趣的问题了。
0,function f1(){}
这样一个语句中,究竟是函数表达式还是函数声明呢?
答案是:两者皆是。
当然这仅是从效果上说,因为本来这样的行为就不符合标准的定义。
不过更微妙的问题是,这个表达式返回的函数对象和所声明的f1函数,其实并非同一个函数!
对此,你可以在IE浏览器的地址栏里执行:javascript:alert(eval('0,function f1(){}')==f1),结果是false。
也就是说function表达式产生了一个不同于function声明的结果——这里产生了两个不同的函数对象!
回想到我以前对于with语句行为的测试,可以确认,JScript 5.0到5.5的时候,对function声明和function表达式的行为做了很大的修订,这可能是上述奇怪表现的源头。不过我手头没有JScript 5.0,所以无法做进一步的确认。
另一个例子是:javascript:alert(eval('(function f1(){return arguments.callee==f1})()'));alert(f1())
返回结果false和true。
注意,这边的不相等,并非COM对象包装所造成的假象(比如两次读取event包装对象不相等,或者跨window/frame所造成的问题),而真的是两个函数,其实际执行效果其实也是不同的(虽然这里这个例子无法表现出来,但是如果加上with语句就会有差别了)。
产生这个用于函数表达式的函数,和函数声明所产生的函数一样,是需要CPU资源的。对此,我写了一个测试:
var MAX = 100000
function timer(f) {
var start = new Date().getTime()
for (var i = 0; i < MAX; i++) {
f()
}
var end = new Date().getTime()
if (typeof alert != 'undefined')
alert(end - start)
else
WScript.Echo(end - start)
}
timer(test0)
timer(test1)
timer(test2)
timer(test3)
timer(test4)
timer(test5)
timer(test6)
timer(test7)
function test0 () {
// do nothing
}
function test1 () {
// func decl
function f() {}
}
function test2 () {
// func decl or exp?
(function f() {})
}
function test3 () {
// two functions
function f1() {}
(function f2() {})
}
function test4 () {
// two references point to one function
var f1, f2
f1 = f2 = function () {}
}
function test5 () {
// still one function
var f1 = f2
function f2() {}
}
function test6 () {
// actually two different functions!
var f2 = function f1() {}
}
function test7 () {
// two functions
var f1 = function () {}
function f2() {}
}
执行结果大体如下:
D:\>cscript test-func-benchmark.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation. All rights reserved.
2704
7593
7563
10047
7828
7735
10079
10078
这个测试从侧面再一次证明了诸如var x=function f(){}这样的语句,实际会产生两个不同的函数。
按照我的猜测,最初的JScript可能是这样处理的:任何函数结构一律按照函数声明提前处理(所以总是产生变量绑定),函数表达式则被替换为函数对象的引用。
后来JScript 5.5为了改善对于with的处理而加入了真正的函数表达式实现,本来它可以完全fix这个问题。但是为了向前兼容而保持对于具名函数一律产生变量绑定,并且也保持了其with作用域的行为(就with语句中的function声明而言,按照规范实际应可视为语法错误——不过SpiderMonkey等引擎都按照function表达式等同处理,而JScript则选择忽略with)。最后得到的就是一个语句产生两份函数对象这样微妙的结果。
进一步我们可以大胆猜想,JScript 5.5大体上是这样:JScript在解析代码时,如果函数有具名,就会产生一份函数对象绑定到对应该名字的变量上,函数声明和绑定会被提前到当前执行上下文的最前端,因此总是会忽略with语句。而如果是一个函数表达式,则到实际运行到包含函数表达式的语句时,如果这个函数表达式有被用到(如有运算符时),就会产生一份函数对象,这个过程是在运行时的,所以会纳入with语句对于scope的修改。这两个步骤是共存的,所以可能出现一个语句最终产生出两份有微妙差异的函数对象的现象(实际上并不一定是两份,因为函数表达式是每运行到该语句时就产生一个函数对象,所以是n+1份)。
最后我们来看单独一句“(function (){})”,它既不是声明语句也不是表达式(严格的说是既没有函数声明的效果,也没有函数表达式的效果),实际上这里什么也没执行,这个代码被 JScript抛弃掉了(或者说这里是一个被优化掉了的函数表达式)。这一点可以通过类似我上面的性能测试证明之(即包含这个语句的函数其执行时间与一个空函数是一样的)。
之所以单独的括号不会触发函数表达式,也许是因为括号只是用于产生AST的结构而不像运算符等会产生独立的节点,JScript没有对此作特别处理,所以单独括号和没有括号并无差别。当然这也只是我的臆测而已。