论坛首页 综合技术论坛

(新增官方回复)对【无类语言的OOP(JavaScript描述) 】一贴中第一个代码段的不同意见

浏览 26038 次
该帖已经被评为良好帖
作者 正文
   发表时间:2007-07-20  
Lich_Ray 写道
我的提醒白提醒了。
引用
还有,让prototype的版本的构造函数中有一次引用赋值,否则就成了只创建对象而没有构造函数执行的版本了。

因为你的第二个版本构造函数执行了100000次,LS!而你的第一个
function Darkness() {
}   

构造函数为空,在被 new 的时候无须执行!只要构造函数中有需要执行且不会被优化掉的语句,时间就会大幅增长。
我测试你的代码,前者3s,后者9s;前者构造函数中有简单引用赋值语句时5s。然后我注意到你的代码中有许多多余的空格,这会大大增加字符串比较的负担(因为它的算法时间复杂度是绝对的theta(n))。去掉之后变成:
this.turnOn=function(){alert("bingo")}

时间竟然下降到5s。我觉得这不符合逻辑。排除可能存在的缓存问题后再测,6s。看来,后者花在检测上的时间才是主要矛盾。
你的这个代码最多能证明,在“超大规模”创建对象时最好还是让构造函数少做点事,快一点但是,函数在内存中确实是只有一个

PS: 至于你测的第二段代码要一分半钟,我估计是这个原因:首先,在浏览器中一次跑90秒是不可能的,它会跳出问是否继续执行的对话框,此时JS代码停止运行,但系统时间仍在继续。联想到你说第一个代码段耗时5秒,据此推测你的机器性能够戗。如果是浏览器IE,其内存管理问题很大,直接崩溃都有可能;如果是Firefox,在Geoko框架下创建对话框的耗时本来就大,再加上Java和JavaScript的垃圾收集器同时运行(关于Java垃圾收集器在极端情况下的性能问题参见《Effective Java》),会因为线程跳转过缓而加剧时间消耗。也就是说,因为一点机器性能不足而导致代码平白无故多跑很久。你觉得我的解释有道理吗?


如我前面所述,你几乎所有解释都是站不住脚的。
1. 目前的主要js引擎都不存在取消空函数调用的优化。
2. 不存在所谓字符串比较。
3. Browser里js执行的每次时间有较大波动(如相差50%)是很正常的,除非累积大量数据,你不能从中得出任何结论。
4. 这里根本不存在任何Java代码。况且Java的垃圾回收再烂,也不会出现你所说的这种情况。

你唯一猜对的一点,是执行那么久确实是与js的垃圾回收有关。因为jscript 5.7之前的版本,貌似在每若干次new之前都会调用GarbageCollection,从而导致new的性能随对象数量的增多而指数下降。
0 请登录后投票
   发表时间:2007-07-20  
首先,这一个帖子中,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同志。
    0 请登录后投票
       发表时间:2007-07-20  
    前面已经说过的部分不再赘述。就解释一下红字部分。他不是说对于function A(){return function(){}},两次A()就可以join,两次new A()就不能join。不是这个意思。它是指,对于function A(){return new Function()},两次调用是不能join的。另外两次eval的call的结果也不能join。

    keshin 写道:
    如果是,我不知道,你是否指的是下面红色的部分。

    13.1.1 Equated Grammar Productions
    Two uses of the FunctionBody grammar production are defined to be equated when one of the following
    is true:
    • Both uses obtained their FunctionBody from the same location in the source text of the same
    ECMAScript program. This source text consists of global code and any contained function codes
    according to the definitions in 10.1.2.
    • Both uses obtained their FunctionBody from the same location in the source text of the same call to
    eval (15.1.2.1). This source text consists of eval code and any contained function codes according
    to the definitions in 10.1.2.
    NOTE
    Two uses of FunctionBody obtained from a call to the Function constructor 15.3.1 and 15.3.2) are
    never equated. Also, two uses of FunctionBody obtained from two different calls to eval are never
    equated, even if those two calls to eval were passed the same argument.

    我很不希望,你指的是这部分。




    0 请登录后投票
       发表时间:2007-07-20  
    最后一段的意思是,实践中对于[[scope]]没有差别的才join可能更高效,因为所有join的情况就变成了自己join自己,也就是使用同一个对象(因为连[[scope]]都一样,那就完全一样了)。

    ie和ff都没有实现join,除了因为join的定义本身存在问题,还因为判断[[scope]]没有可观测的差异实际上并不容易。
    至于说相等性测试对于函数无意义,我想你现在明白他的意思了,规范里是不会有这一条的。

    keshin 写道:

    Step1 允许一个具体实现在在function A有一个不依赖于A的function B时进行优化。注意是允许

    在给出的第一个例子中,规范也仅仅是说,允许但不需要把b1和b2 join。

    再看最后一段,只有在具体实现能够证明两个function Object的scope属性的区别是看不出来的(真饶)情况下,两个function Object才可以join。根据这个规则,恐怕实现只能找到对象它自己是可已join的。

    在回头看我们的相等性测试

    两个值是同一对象,或者是可以join的,而关于这个join,规则本身也仅仅是一个非常宽松的规定。

    再来看我引用的这对规范里面的这个例子,在客户端b1和b2能通过相等性测试么?很遗憾,至少在ie和ff,它都不能,也就是说

    这两个客户端貌似没有实现这个规范。

    至于说相等性测试对于函数是无意义的,一定返回false的,我希望谁能够告诉我具体在规范的哪里?




    0 请登录后投票
       发表时间:2007-07-20  
    Lich_Ray 写道

    至于你最后说的 ECMA 的回复,他们说的很客观,意思是优化未必启动。我想了一下,觉得这可能和数据流分析的结果有关,而数据流分析会受运行时情况影响,而且是个概率算法,优化是否启动因此不确定。这是我的猜测,可能错得很彻底,因为我也不可能对 Rhino 研究很透;而且如果谈到这些,那就和你的初衷背离了不止一点了,刨根问底也要有个度嘛。


    ECMA的人的回复所谓“优化未必启动”,并非在说join,而是说其局部变量在某些情况下可以被优化为用stack,而不是heap。理论上这种优化是可行的,而且是确定的,并非什么概率算法。但是实际中,我倒是确实不知道有哪些implementation是作这种优化的。也许Rhino的js to java byte code之后,jvm会作这种优化。
    0 请登录后投票
       发表时间:2007-07-20  
    引用
    说白了,就是必须是从同一个函数体产生出来的。这里哪里需要字符串比较呢?由此,你后面的解说也属无效。
    ...
    如果你不是把所有创建的对象都放入数组。而是仅仅new XXX(); 则这些对象一被创建出来就可被垃圾回收。这样测试结果相差很少。

    非常明显,这是我的猜测;因为我不能理解 Rhino 解释器的优化在捕获数组环境时这么有意思;所以我也不再想提这一点。
    引用
    就我所知的目前所有主流的js引擎,优化力度都是相当小的,不存在把空函数调用取消这样的优化。

    这个就要推敲一下了。根据闭包算法,调用一个空函数无须链接逃逸变量,效率极高,即使不优化;所有我说的是“相当于没有”。
    引用
    你说的是没错。但是你的表达不严谨。既然不 ===,就不能说是“相等性”。

    这个就是你的理解错误了:equated 译为“相等”是当然的。
    引用
    如果按照规范的话,等价function会被joined起来,并且===比较会是true。就这点而言,我认为规范本身是有问题的。
    ...
    如前所述,规范本身可能存在缺陷,导致语义可变。所以所有的实现都选择不join。

    规范本身一点问题没有,是你想的太天真了。语义上出现两可解释是不能满足等式推理的语言的可怜的特点,错不在规范。
    引用
    Browser里js执行的每次时间有较大波动(如相差50%)是很正常的,除非累积大量数据,你不能从中得出任何结论。

    我已经重启计算机排除缓存问题,这个还是免提了吧。
    引用
    这里根本不存在任何Java代码。况且Java的垃圾回收再烂,也不会出现你所说的这种情况。

    这个还是要斟酌一下;不过有一点可以肯定,我没碰到过要一分半钟这种问题。
    引用

    实际上看到这里,你应该可以看出两个turnOn根本就指向的是同一个对象,即函数test。所以当然是相等的。不等才怪!
    其次,如前所说,这里根本没有两个test,只有一个test,然后有若干个对test的引用。

    我认为这里逻辑没有错误。被join之后,对象当然只会剩下一个,于是我问被join吗,能所以返回true啊,我只是少些一句话啊。
    引用
    SpiderMonkey会在每个function(设为X)里面预先产生一个共享的function(设为Y)。所有X里直接包含的function都会以Y作为__proto__,而这些不同的function对象,只有[[scope]]是不同的。这就是一种优化,而且很美妙的使用了js自己的 prototype chain的机制。

    说了这么多张帖,就这一张是有用的,其它的其实懂的人心里都很清楚,不要埋怨别人写的这里那里不好。
    0 请登录后投票
       发表时间:2007-07-20  

    hax 写道:
    前面已经说过的部分不再赘述。就解释一下红字部分。他不是说对于function A(){return function(){}},两次A()就可以join,两次new A()就不能join。不是这个意思。它是指,对于function A(){return new Function()},两次调用是不能join的。另外两次eval的call的结果也不能join。

    keshin 写道:
    如果是,我不知道,你是否指的是下面红色的部分。

    13.1.1 Equated Grammar Productions
    Two uses of the FunctionBody grammar production are defined to be equated when one of the following
    is true:
    • Both uses obtained their FunctionBody from the same location in the source text of the same
    ECMAScript program. This source text consists of global code and any contained function codes
    according to the definitions in 10.1.2.
    • Both uses obtained their FunctionBody from the same location in the source text of the same call to
    eval (15.1.2.1). This source text consists of eval code and any contained function codes according
    to the definitions in 10.1.2.
    NOTE
    Two uses of FunctionBody obtained from a call to the Function constructor 15.3.1 and 15.3.2) are
    never equated. Also, two uses of FunctionBody obtained from two different calls to eval are never
    equated, even if those two calls to eval were passed the same argument.

    我很不希望,你指的是这部分。

    呵呵,一天没来,想不到这个贴又加了那么多,另外我不知道你这块是说给我听的还是他听的,只想说,我想表达就是你这个意思。
    另外很佩服你语言组织的逻辑能力,这和修为有关,我还得加强。






    0 请登录后投票
       发表时间:2007-07-20  
    Lich_Ray 写道

    引用
    就我所知的目前所有主流的js引擎,优化力度都是相当小的,不存在把空函数调用取消这样的优化。

    这个就要推敲一下了。根据闭包算法,调用一个空函数无须链接逃逸变量,效率极高,即使不优化;所有我说的是“相当于没有”。


    首先空函数优化和闭包优化是不同层次的优化。而我说的是不存在空函数优化。其次你所说的这种对于闭包的优化,实际上不限于空函数,就是说它可以被视作与其若干的外层的function等所有scope无关,按照规范的语言来说,就是该function的[[scope]]与某个更外层上的function的[[scope]],没有可以观察到的差别。

    但是,我认为,这种优化对于执行效率的提高是极其有限,仅仅能提高一些name resolve的速度,几乎可以忽略不计。它只是有助于更高效的垃圾回收。而现有的引擎是否会做这个优化,是非常可疑的。就我所知,只有SpiderMonkey会在非常罕见的条件下,才会执行这个优化。Rhino是不会的。其他引擎不详。

    0 请登录后投票
       发表时间:2007-07-20  
    终于看完了,还是挺精彩的。
    有些部分写得还是挺好。
    0 请登录后投票
       发表时间:2007-07-20  
    Lich_Ray 写道

    引用
    你说的是没错。但是你的表达不严谨。既然不 ===,就不能说是“相等性”。

    这个就是你的理解错误了:equated 译为“相等”是当然的。


    你不要想当然。技术讲求精确性,特别是在说规范的时候。显然equal和equated的意思是不一样的,否则规范何必用两个词?我们通常把equal作为“相等”,那么对于equated,你就不能马马虎虎的说是“相等”。

    equated强调“换算”的意思,是“视作等同”,或可译作“等价的”,但不是“相等的”(equal),更不能是“相同的”(same)。

    而你对这几个词汇的使用是乱做一团。

    正是因为你混淆了equated和equal的含义,所以才会闹出“识别相等要进行函数代码字符串比较”这样的笑话。

    我说你表达不严谨已经是很客气了。
    0 请登录后投票
    论坛首页 综合技术版

    跳转论坛:
    Global site tag (gtag.js) - Google Analytics