ECMAScript规范可能存在缺陷

原讨论帖地址是[url]http://www.iteye.com/topic/101506[/url]

首先,这一个帖子中,Lich_Ray按照ECMA规范来推导,这个尝试是好的,但是ECMAScript的规范是出了名的佶屈聱牙,所以不幸的,Lich_Ray同志也未幸免。。。


[quote="Lich_Ray"]看来我就此开个 JavaScript 专栏一心和你们讨论这些问题算了。这帖子居然还有一群人跟在后面打“良好”“精华”,无语了…

火是要发的,问题也是要解决的。下面讲正经的。不仅仅讲给 keshin 一个人听,大家都来看下吧。我们开始从 === 运算符谈起。

请翻到 ECMA-262 e4 的 11.9.4 节,The Strict Equals Operator (===)。这里给出了 === 运算符的语法产生式和展开(注意,只是部分求值)步骤:

EqualityExpression === RelationalExpression is evaluated as follows:
[list=1]
[*]Evaluate EqualityExpression.
[*]Call GetValue(Result(1)).
[*]Evaluate RelationalExpression.
[*]Call GetValue(Result(3)).
[*]Perform the comparison Result(4) === Result(2). (See below.)
[*]Return Result(5).
[/list]

它指出,对两个产生式的求值结果调用规范中的算法 GetValue(V)。那么翻到这一节,8.7.1 GetValue(V)。
[list=1]
[*]If Type(V) is not Reference, return V.
[*]Call GetBase(V).
[*]If Result(2) is null, throw a ReferenceError exception.
[*]Call the [[Get]] method of Result(2), passing GetPropertyName( V) for the property name.
[*]Return Result(4).
[/list]
根据第一句,判断 Type(V) 是不是引用。现在要分情况讨论一下。先解决你上一帖提出的问题。在你给出的构造函数中,需要执行的是这样一句:
[code]
this.turnOn = test;
[/code]
那么,检查 Type(this.turnOn),是到一个 Object Type 的函数 test的引用。于是调用 GetBase(V) 获取到 trunOn 引用的基对象 this,此描述用函数的定义在 8.7 The Reference Type 中:
[list]
[*] GetBase(V). Returns the base object component of the reference V.
[/list]
对象 this 不是 [b]null[/b],跳转第4步调用 8.6.2.1 节中 [[Get]](P) 算法,传递的属性名是 turnOn。根据算法

[list=1]
[*]If O doesn't have a property with name P, go to step 4.
[*]Get the value of the property.
[*]Return Result(2).
[*]...
[/list]

取到该属性的对应值,即内部记录的函数 test。现在我们知道了,是两个 test 在作比较。(友情提醒:请继续往下看。)那么返回 11.9.4 节。

现在只有 Step(5): Result(4) === Result(2). 是最重要的。旁边的(See below.)指出 === 的算法在下文,11.9.6 节 The Strict Equality Comparison Algorithm。在这一节我们看到了具体的算法。前面12步全是废话,看第13步:
[list=13]
[*] Return true if x and y refer to the same object or if they refer to objects joined to each other (see 13.1.2). Otherwise, return false.
[/list]

[/quote]

以上都ok。

实际上看到这里,你应该可以看出两个turnOn根本就指向的是[b]同一个对象[/b],即函数test。所以当然是相等的。不等才怪!

可惜的是,Lich同志不知道是没有细看源代码,还是实在按捺不住解说那个复杂的joined function object的冲动,就直接奔向这“只剩这一种可能了”。

[quote="Lich_Ray"]
很明显,如果两个函数对象(只剩这一种可能了)可以被 joined,它们就相等。那么根据提示,翻到 13.1.2 Joined Objects。
下面是函数对象可被 joined 的条件:
[list]
[*] Any time a non-internal property of an object O is created or set, the corresponding property is immediately also created or set with the same value and attributes in all objects joined with O.
[*] Any time a non-internal property of an object O is deleted, the corresponding property is immediately also deleted in all objects joined with O.
[*] ...(这两句跟主题无关)
[/list]
这两句话罗嗦死了,其实就四个字:“同生同死”——要求可被 joined 的对象们的属性要产生都产生,要删除都删除。现在以此条件看 test 函数,符合以上条件,可被 joined,于是 === 运算符返回 [b]true[/b]。
[/quote]

注意,你所摘录的只是Joined Objects的行为,而不是Joined的条件!所以你的逻辑首先有问题,就是因果颠倒了。其次,如前所说,这里根本没有两个test,只有一个test,然后有若干个对test的引用。

[quote="Lich_Ray"]

解决下一个问题,你曾经写过的返回 [b]false[/b] 的代码:
[code]
function Light() {
this.turnOn = function () {
alert("bingo");
}
}

var lightA = new Light();
var lightB = new Light();

alert(lightA.turnOn === lightB.turnOn);
[/code]
前面步骤相同不用看了,只看最后一个步骤,
[code]
function () {
alert("bingo");
}
[/code]
函数们能否被 joined?当然不能,用肚脐眼都能看地很清楚,修改 lightA.turnOn 上的属性不会影响 lightB.turnOn。

[/quote]

这里是你本篇的最大错误。“修改lightA.turnOn的属性不会影响lightB.turnOn“,这是因为它们是两个不同的对象,因而也不可能是被join的,但是这是结果,而不是原因!而且就算反推回去,也是不成立的。因为join只是可选的,并非强制的。

事实上,规范给出了一个可以join的例子如下(大意):

function A() {
function B(x) { return x*x }
return B
}
var a1 = A();
var a2 = A();

规范说:a1和a2是equate的,可以被join起来,并且因为他们的[[scope]]也没有可以观察到的差异,所以甚至可以指向same object,而不是两个被join起来的object。

首先我要指出,规范本身也不是完美的。就我本人来看,让a1和a2指向同一个对象,是很奇怪的一件事情。而且这个例子中,[[scope]]是没有差异的,更明确的说function B是不依赖A的。如果按照这个标准看, function () { alert("bingo"); }当然也是可以被指向同一个对象的!这实际上意味着lightA.turnOn和lightB.turnOn可以是同一个对象,因此你修改lightA.turnOn的属性,也就等于修改lightB.turnOn的属性。

这很奇怪,但是你不能因为奇怪就反推出他们不能join。BTW,你没有特异功能,所以你还是不要用肚脐眼来看代码。

进一步,在B依赖A的情况下,B也是可以join的。因为B依赖A,不过就是多个B的[[scope]]不同,而这是允许的(见13.1.2 Joined Object的NOTE部分)。

所以如果严格根据规范,implmentation选择join或者不选择join,实际上会导致差异,而且是语义上的差异。所以我认为这是规范的一个错误!!

当然,要确认这一点,我是不够权威的,我打算有空的时候,写个信求教一下BE。

事实上,我所知的所有实现,都没有像规范所描述的join行为。也就是规范所举的那个a1和a2,在现实我所实验过的任何一个引擎里,都是不==的。

[quote="Lich_Ray"]

问题解决完了,下面解释我提到过的函数相等性测试。第 13.1.1 节,Equated Grammar Productions 中的内容已经讲过,没看见拉倒。下面翻到课本:
这一节的算法中,用到函数相等性测试的只有 Step1
[list=1]
[*] If there already exists an object E that was created by an earlier call to this section's algorithm, and if that call to this section's algorithm was given a FunctionBody that is equated to the FunctionBody given now, then go to step 13. (If there is more than one object E satisfying these criteria, choose one at the implementation's discretion.)
[/list]
这一段话的意思很明显:如果函数相等性测试可用,就配合括号中的句子(If there is more than one object E satisfying these criteria, choose one at the implementation's discretion.)做优化;如果不可用,Step 1 忽略。

这里有一个隐含的内容:是否优化对语言语义无影响。这涉及编译原理的体系,即:优化措施属于实现层,实现层不可影响由语言规范所定义的语义层,即语言的外在表现。说白了,就是“规范是上帝,实现只能是上帝的羊羔,随你怎么折腾,不可僭越”,否则就是实现失败。所以我看到 keshin 这个第一帖中的代码时,想都不想就知道是语义的结果,跟实现无关。
[/quote]

如前所述,规范本身可能存在缺陷,导致语义可变。所以所有的实现都选择不join。

[quote="Lich_Ray"]
以 Rhino 解释器(也就是 Firefox 的解释器啦)为例,一个函数对象(JavaScript 中函数没有对应原始类型,就是(is a) Object)在解释器中由两部分构成:一是可执行的继承 Scriptable,Callable 接口的 Java 类 BaseFunction 对象,外部 wrap 是 FunctionObject 类的对象。相等性测试通过的函数们,都是堆上的一个 BaseFunction,被存储为一个 FunctionNode,作为 JavaScript 函数执行时只调用一段代码块;但执行 === 比较时,只比较其在 JavaScript 中的外在表现,即 FunctionObject 是否相等,结果就是这样有些“奇怪”的表现。
[/quote]

Rhino的具体实现,与Joined Object,是没有直接关系的。或者说,Rhino实现的其实是所有equated的函数都是一个BaseFunction对象。实际上,所有的现实js实现都会采取类似的策略。

特别的,我们在SpiderMonkey(就是FF等用的js引擎)中可以看到这种痕迹:
function A() {
return function() {}
}

你会发现,A().__proto__不等于Function.prototype(由于__proto__并没有说就是[[prototype]],所以也不能说spidermonkey不合规范)。
但是两次调用的结果是相同的。也就是 A().__proto__ == A().__proto__,并且A().__proto__.__proto__ == Function.prototype。

也就是说,SpiderMonkey会在每个function(设为X)里面预先产生一个共享的function(设为Y)。所有X里直接包含的function都会以Y作为__proto__,而这些不同的function对象,只有[[scope]]是不同的。这就是一种优化,而且很美妙的使用了js自己的prototype chain的机制。

Safari有__proto__属性,但是就没有上述现象(但是它仍可以在内部有一个类似的机制)。


[quote="Lich_Ray"]

讲解结束,下面是我对这个问题的个人意见。
我给出的代码,从效率角度来看,不是最优的;但从上文的叙述中可以看出,可以这样理解:
[list]
[*] 这可以作为一种不同于 prototype 的“新”的给对象绑定方法的形式,更加安全。对于这种观点和使用方式效率没有任何问题。
[*] 当作普通 prototype 的另一种写法来使用,相对于在代码中多消耗了一个 O(n) 级的算法,而且此算法来自 JavaScript 底层实现且可被加速,效率下降忽略不计,去掉 JS 代码中所有为空的行、无效空格,把所有换行换成 ; 损失就回来了;仅当处于“大规模的”循环中时才可视为一个 O(n) 算法,这种情况存在的可能性微乎其微,不过,还是那句话,尽量避免。
[/list]

我的话说完了。请注意,下面不是答疑时间。

PS: 刚刚才看到 keshin 原帖新加的内容。ECMA 的人说的非常正确,不过他们不敢解释太多;态度很客气,这一点让我非常佩服。[/quote]

最后,回复keshin的不知道是哪位。他所描述的也就是一个实现的一般思路。但是说到规范的本意,只有问当时的参与制定者才能了解。例如BE同志。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值