ECMAScript规范可能存在缺陷

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

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


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

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

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

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


它指出,对两个产生式的求值结果调用规范中的算法 GetValue(V)。那么翻到这一节,8.7.1 GetValue(V)。
  1. If Type(V) is not Reference, return V.
  2. Call GetBase(V).
  3. If Result(2) is null, throw a ReferenceError exception.
  4. Call the [[Get]] method of Result(2), passing GetPropertyName( V) for the property name.
  5. Return Result(4).

根据第一句,判断 Type(V) 是不是引用。现在要分情况讨论一下。先解决你上一帖提出的问题。在你给出的构造函数中,需要执行的是这样一句:
	this.turnOn = test;

那么,检查 Type(this.turnOn),是到一个 Object Type 的函数 test的引用。于是调用 GetBase(V) 获取到 trunOn 引用的基对象 this,此描述用函数的定义在 8.7 The Reference Type 中:
  • GetBase(V). Returns the base object component of the reference V.

对象 this 不是 null,跳转第4步调用 8.6.2.1 节中 [[Get]](P) 算法,传递的属性名是 turnOn。根据算法

  1. If O doesn't have a property with name P, go to step 4.
  2. Get the value of the property.
  3. Return Result(2).
  4. ...


取到该属性的对应值,即内部记录的函数 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]



    以上都ok。

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

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

    Lich_Ray 写道

    很明显,如果两个函数对象(只剩这一种可能了)可以被 joined,它们就相等。那么根据提示,翻到 13.1.2 Joined Objects。
    下面是函数对象可被 joined 的条件:
    • 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.
    • ...(这两句跟主题无关)

    这两句话罗嗦死了,其实就四个字:“同生同死”——要求可被 joined 的对象们的属性要产生都产生,要删除都删除。现在以此条件看 test 函数,符合以上条件,可被 joined,于是 === 运算符返回 true


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

    Lich_Ray 写道


    解决下一个问题,你曾经写过的返回 false 的代码:
    function Light() {
    	this.turnOn = function () {
    		alert("bingo");
    	}
    }
    
    var lightA = new Light();
    var lightB = new Light();
    
    alert(lightA.turnOn === lightB.turnOn); 
    

    前面步骤相同不用看了,只看最后一个步骤,
    	function () {
    		alert("bingo");
    	}
    

    函数们能否被 joined?当然不能,用肚脐眼都能看地很清楚,修改 lightA.turnOn 上的属性不会影响 lightB.turnOn。



    这里是你本篇的最大错误。“修改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,在现实我所实验过的任何一个引擎里,都是不==的。

    Lich_Ray 写道


    问题解决完了,下面解释我提到过的函数相等性测试。第 13.1.1 节,Equated Grammar Productions 中的内容已经讲过,没看见拉倒。下面翻到课本:
    这一节的算法中,用到函数相等性测试的只有 Step1
    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.)

    这一段话的意思很明显:如果函数相等性测试可用,就配合括号中的句子(If there is more than one object E satisfying these criteria, choose one at the implementation's discretion.)做优化;如果不可用,Step 1 忽略。

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


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

    Lich_Ray 写道

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


    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__属性,但是就没有上述现象(但是它仍可以在内部有一个类似的机制)。


    Lich_Ray 写道


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


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

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


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

    你可能感兴趣的:(JavaScript,算法,prototype,firefox,Safari)