该帖已经被评为良好帖
|
|
---|---|
作者 | 正文 |
发表时间:2007-07-16
几天前一个同事把这篇文章发给我看,当时我看到第一个代码的时候就有不同意见,实在不吐不快,不过由于论坛要注册三天后才能发帖,所以只有等到今天才能发贴。 原文地址:http://www.iteye.com/topic/89554 代码
回帖中也有网友提出不同意见,不过Lich_Ray给出的回复是这样的: dennis_zane 写道
第一个例子中在构造函数中创建的函数,很多javascript的书都说这样创建的每个对象都有自己的函数版本,比如这里的Light的每个对象都有自己的turnOn,楼主说“JavaScript 没傻到给每个对象都真去分配一个函数的地步“,这一点如何证明?
引用
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
引用
Step 1 allows an implementation to optimise the common case of a function A that has a nested function B where B is not dependent on A. In this case the implementation is allowed to reuse the same object for B instead of creating a new one every time A is called. Step 13 makes this optimisation optional; an implementation that chooses not to implement it will go to step 2.
PS: 多态对于 JavaScript 来说自动存在。看标准理解点运算符和访问运算符 [] 的行为(自动查找继承链);附件中也有一点浅显的介绍。从我写的 Mixin 代码中也能看出来:PhilipLight 不就是多个 price 属性吗?本来跟 Product 类要求的 price 并不相关,但照用不误。 事实是否如此?我们来看一段代码: js 代码
在第一个alert处(第十行),返回的false,而在第二个alert处(22行),返回的是true。 也就是说,第一个类定义的方式,在实例化的时候js会为每个对象分配一个内存空间。而使用prototype则不会有这样的问题。 以上代码在ie和ff上面的运行结果的是一样的。 从规范看,Lich_Ray说的似乎没有问题,我没有仔细看过规范,也不是很懂什么编译原理,但是我知道对于我们实际开发中还是得以客户端的实际实现为依据 昨天我就这个问题向ecma写了封求助信,这是他们的答复(仅修改本人的email地址): This all depends somewhat on the implementation, but in a typical and 我的理解是,编译的代码确实只有一份,但是 the environment is variable, and therefore the 也就是说每个function是不同的,因为,call(调用)和实例化却是不同的,实例化是一个带有一定持久性的概念。规范关于此的陈述也仅仅是call,注意,全部是call。 不知Lich_Ray的理解如何? 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2007-07-16
这个问题,在很多js书籍上也讲了,如果在构造函数里面写的话是会浪费内存的,所以我觉得还是尽量避免的好,用prototype得了……
|
|
返回顶楼 | |
发表时间:2007-07-16
LS,不说你了。你可能真的需要充电充电了。
首先,根据我列出的 ECMAScript 规范,识别“相等”函数需要进行函数代码的字符串比较。为了测试函数是被创建了还是只有一个引用,二者必须尽量排除函数体比较这个时间变量,采用空函数体(使用有函数体的代码时,时间会有微小的差别,但已经能做到不影响你的“客户端”应用了)。 那么,来测测这两段代码的效率: // 这个能计算出引用赋值500000次所消耗的时间,5秒钟 fun = function () {} a = [] time = {} time.org = new Date().toTimeString() for (var i = 0; i < 500000; i++) { a[i] = fun } time.after = new Date().toTimeString() alert(time.org+'\n'+time.after) // 假如按你所说,没有函数相等性识别机制,这段代码就完了;然而,它也耗时5秒钟 a = [] time = {} time.org = new Date().toTimeString() for (var i = 0; i < 500000; i++) { a[i] = function () { this.on = false } } time.after = new Date().toTimeString() alert(time.org+'\n'+time.after) 你既然这么能质疑,还可以profile对象创建时,我给出的“逐个绑定”的代码效率如何,别忘了给字符串比较算法作算法时间复杂度分析;还有,让prototype的版本的构造函数中有一次引用赋值,否则就成了只创建对象而没有构造函数执行的版本了。 现在来解释你所说的函数比较返回 false 什么的问题。这根本就不是问题。对于任意两个函数,规范指出,函数比较必须返回 false;在进行引用赋值时,首先把函数封装为对象,引用比较执行函数对象的比较,返回 true。我所说的函数的相等性测试,指的是 JavaScript 实现中的一种机制(所有 ECMAScript 规范中表明“build-in”的章节都属此类),与语言的外在表现无关;在这里就是相等性测试与 === 测试无关,因为二者根本就不是同一概念。一个语言实现的优化,目的就是要在你从外部看不出来的情况下提高性能;现在看到的宏观表现,必然是语言语义层所反映的内容而非未优化的后果。 |
|
返回顶楼 | |
发表时间:2007-07-16
Lich_Ray 写道 现在来解释你所说的函数比较返回 false 什么的问题。这根本就不是问题。对于任意两个函数,规范指出,函数比较必须返回 false;在进行引用赋值时,首先把函数封装为对象,引用比较执行函数对象的比较,返回 true。我所说的函数的相等性测试,指的是 JavaScript 实现中的一种机制(所有 ECMAScript 规范中表明“build-in”的章节都属此类),与语言的外在表现无关;在这里就是相等性测试与 === 测试无关,因为二者根本就不是同一概念。一个语言实现的优化,目的就是要在你从外部看不出来的情况下提高性能;现在看到的宏观表现,必然是语言语义层所反映的内容而非未优化的后果。 意思是: 按照规范,=== 两个函数比较返回false,同引用返回true。 后者prototye里函数的定义好理解, 前者function内定义函数,肯定是两个函数了,按照规范必须返回false。但是实际实现的时候,他们是在内存中是同一个函数。 是这么了解吧? |
|
返回顶楼 | |
发表时间:2007-07-16
那我们按3楼的说法来做个测试: js 代码
结果很遗憾,前面那个花了5秒钟左右,而后面这个花了我一分半钟。 能给我解释一下么? |
|
返回顶楼 | |
发表时间:2007-07-16
我的提醒白提醒了。
引用 还有,让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》),会因为线程跳转过缓而加剧时间消耗。也就是说,因为一点机器性能不足而导致代码平白无故多跑很久。你觉得我的解释有道理吗? |
|
返回顶楼 | |
发表时间:2007-07-16
我的机器确实是够呛,不过跑九十秒却是真的,而且是在没有跳出警告框的情况下,我现在正在看规范,在得出我的结论前,我先收回不是同一个内存的观点,不过我还是认为你原先的代码并不是最优化的。正如你说的“后者花在检测上的时间才是主要矛盾。”,你认为呢
PS."对于任意两个函数,规范指出,函数比较必须返回 false;"不知道你能否告诉我这个是在规范那里规定的?我找不到诶,而且假如真是如此,以下代码的比较同样应该返回false: function test() {alert("bingo");} function Light() { this.turnOn = test; } var a = new Light(); var b = new Light(); alert(a.turnOn === b.turnOn); alert(a.turnOn === test) 但很遗憾,这个比较返回的是true。 |
|
返回顶楼 | |
发表时间:2007-07-17
看来我就此开个 JavaScript 专栏一心和你们讨论这些问题算了。这帖子居然还有一群人跟在后面打“良好”“精华”,无语了…
火是要发的,问题也是要解决的。下面讲正经的。不仅仅讲给 keshin 一个人听,大家都来看下吧。我们开始从 === 运算符谈起。 请翻到 ECMA-262 e4 的 11.9.4 节,The Strict Equals Operator (===)。这里给出了 === 运算符的语法产生式和展开(注意,只是部分求值)步骤: EqualityExpression === RelationalExpression is evaluated as follows:
它指出,对两个产生式的求值结果调用规范中的算法 GetValue(V)。那么翻到这一节,8.7.1 GetValue(V)。
根据第一句,判断 Type(V) 是不是引用。现在要分情况讨论一下。先解决你上一帖提出的问题。在你给出的构造函数中,需要执行的是这样一句: this.turnOn = test; 那么,检查 Type(this.turnOn),是到一个 Object Type 的函数 test的引用。于是调用 GetBase(V) 获取到 trunOn 引用的基对象 this,此描述用函数的定义在 8.7 The Reference Type 中:
对象 this 不是 null,跳转第4步调用 8.6.2.1 节中 [[Get]](P) 算法,传递的属性名是 turnOn。根据算法
取到该属性的对应值,即内部记录的函数 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] 很明显,如果两个函数对象(只剩这一种可能了)可以被 joined,它们就相等。那么根据提示,翻到 13.1.2 Joined Objects。 下面是函数对象可被 joined 的条件:
这两句话罗嗦死了,其实就四个字:“同生同死”——要求可被 joined 的对象们的属性要产生都产生,要删除都删除。现在以此条件看 test 函数,符合以上条件,可被 joined,于是 === 运算符返回 true。 解决下一个问题,你曾经写过的返回 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。 问题解决完了,下面解释我提到过的函数相等性测试。第 13.1.1 节,Equated Grammar Productions 中的内容已经讲过,没看见拉倒。下面翻到课本: 这一节的算法中,用到函数相等性测试的只有 Step1
这一段话的意思很明显:如果函数相等性测试可用,就配合括号中的句子(If there is more than one object E satisfying these criteria, choose one at the implementation's discretion.)做优化;如果不可用,Step 1 忽略。 这里有一个隐含的内容:是否优化对语言语义无影响。这涉及编译原理的体系,即:优化措施属于实现层,实现层不可影响由语言规范所定义的语义层,即语言的外在表现。说白了,就是“规范是上帝,实现只能是上帝的羊羔,随你怎么折腾,不可僭越”,否则就是实现失败。所以我看到 keshin 这个第一帖中的代码时,想都不想就知道是语义的结果,跟实现无关。 以 Rhino 解释器(也就是 Firefox 的解释器啦)为例,一个函数对象(JavaScript 中函数没有对应原始类型,就是(is a) Object)在解释器中由两部分构成:一是可执行的继承 Scriptable,Callable 接口的 Java 类 BaseFunction 对象,外部 wrap 是 FunctionObject 类的对象。相等性测试通过的函数们,都是堆上的一个 BaseFunction,被存储为一个 FunctionNode,作为 JavaScript 函数执行时只调用一段代码块;但执行 === 比较时,只比较其在 JavaScript 中的外在表现,即 FunctionObject 是否相等,结果就是这样有些“奇怪”的表现。 讲解结束,下面是我对这个问题的个人意见。 我给出的代码,从效率角度来看,不是最优的;但从上文的叙述中可以看出,可以这样理解:
我的话说完了。请注意,下面不是答疑时间。 PS: 刚刚才看到 keshin 原帖新加的内容。ECMA 的人说的非常正确,不过他们不敢解释太多;态度很客气,这一点让我非常佩服。 |
|
返回顶楼 | |
发表时间:2007-07-17
临下班的时候看到Lich_Ray回帖,但是无奈mm在楼下等,只好把回帖拖到现在。 首先请Lich_Ray消消气,精华也好,良好也好,不过是抛给追求真相的过程而已。如果把这些浮云贴到自己身上,未免太过那个了一点。 好了,言归正传。 1、哥们的回帖中提到:“现在以此条件看 test 函数,符合以上条件,可被 joined,于是 === 运算符返回 true。” 之后又向我的肚脐眼挑战说:“函数们能否被 joined?当然不能,用肚脐眼都能看地很清楚,修改 lightA.turnOn 上的属性不会影响 lightB.turnOn。” 哥们,函数倒地能不能被join啊? 2、哥们说“下面解释我提到过的函数相等性测试。第 13.1.1 节,Equated Grammar Productions 中的内容已经讲过,没看见拉倒。”,不知道你这个是否是打算作为前面提到的“对于任意两个函数,规范指出,函数比较必须返回 false”的回答。 如果是,我不知道,你是否指的是下面红色的部分。 13.1.1 Equated Grammar Productions 我很不希望,你指的是这部分。 3、按照ecma的回复,This all depends somewhat on the implementation, but in a typical and 咱们姑且认为这个回复是正确的,那么也就是存在两个部分,编译后的代码,和环境。在编译后的代码中,确实,每个function 确实只有一个,而且也只能有一个。但是对于环境来说,却并非如此。 In practice, implementations optimize this when 也就是说,实现只在他们可以的时候进行优化,而在有些情况下,实现无法做到优化,在我发给他们的例子中,由于,调用Light后create的东西比调用本身活的久。 |
|
返回顶楼 | |
发表时间:2007-07-17
再来谈你的join的问题: Step 1 allows an implementation to optimise the common case of a function A that has a nested function B js 代码
这是在Creating Function Objects时第一步的解释。 大概翻译一下: Step1 允许一个具体实现在在function A有一个不依赖于A的function B时进行优化。注意是允许 在给出的第一个例子中,规范也仅仅是说,允许但不需要把b1和b2 join。 再看最后一段,只有在具体实现能够证明两个function Object的scope属性的区别是看不出来的(真饶)情况下,两个function Object才可以join。根据这个规则,恐怕实现只能找到对象它自己是可已join的。 在回头看我们的相等性测试 两个值是同一对象,或者是可以join的,而关于这个join,规则本身也仅仅是一个非常宽松的规定。 再来看我引用的这对规范里面的这个例子,在客户端b1和b2能通过相等性测试么?很遗憾,至少在ie和ff,它都不能,也就是说 这两个客户端貌似没有实现这个规范。 至于说相等性测试对于函数是无意义的,一定返回false的,我希望谁能够告诉我具体在规范的哪里? |
|
返回顶楼 | |