在JavaScript中,通过“构造函数”创建对象时,可以在构造函数中通过直接给“this”引用添加新的属性来给要创建的对象添加新属性;也可以给构造函数的“prototype”添加属性,这样所有属于此类的对象也都会继承“prototype”的属性。
就运行的效果来说,两种方式结果类似。但是,就代码的格式风格上来看,我本来更倾向于前一种方式,因为所有的东西可以在一个function中写完;而后一种方式则需要在构造函数function声明之后,再另外给其prototype添加其它属性,感觉代码太过松散。
但是如果看完下面的试验,我现在更加推荐后一种处理方式:
< head >
< title > JS属性创建存储的优化 </ title >
< script type ="text/javascript" >
<!--
function Class1() {
this .member = function () {alert( " Class1 " );};
}
function Class2() {
function inner() {alert( " Class2 " );}
this .member = inner;
}
function Class3() {}
Class3.prototype.member = function () {alert( " Class3 " );}
window.onload = function () {
var oTable = document.createElement( " table " );
var oTr, oTd;
var classes = [Class1, Class2, Class3];
var Class;
var o1;
var o2;
function addRow(description, result) {
with (oTable) {
with (insertRow( - 1 )) {
insertCell( - 1 ).appendChild(document.createTextNode(description));
with (insertCell( - 1 )) {
with (appendChild(document.createTextNode(result))) {
if (result === true ) {
style.cssText = " color: blue; " ;
} else if (result === false ) {
style.cssText = " color: red; " ;
} else if (result === undefined) {
style.cssText = " background-color: silver; color: gray; " ;
}
}
}
}
}
}
with (oTable) {
border = 1 ;
style.cssText = " font-size: 12px; " ;
for ( var e in classes) {
Class = classes[e];
o1 = new Class();
o2 = new Class();
with (insertRow( - 1 )) {
with (insertCell( - 1 )) {
style.cssText = " font-weight: bold; " ;
colSpan = 2 ;
appendChild(document.createElement( " pre " ))
.appendChild(document.createTextNode(Class));
}
}
addRow( " member of function prototype " , Class.prototype.member);
addRow( " member of first instance " , o1.member);
addRow( " member of second instance " , o2.member);
addRow( " two members equal? " , o1.member == o2.member);
addRow( " two members strict equal? " , o1.member === o2.member);
}
}
document.body.appendChild(oTable);
}
// -->
</ script >
</ head >
< body >
</ body >
</ html >
IE6运行结果(FireFox2得到同样的结果,仅仅格式有些小不同):
function Class1() { this.member = function() {alert("Class1");}; } | |
member of function prototype | undefined |
member of first instance | function() {alert("Class1");} |
member of second instance | function() {alert("Class1");} |
two members equal? | false |
two members strict equal? | false |
function Class2() { function inner() {alert("Class2");} this.member = inner; } | |
member of function prototype | undefined |
member of first instance | function inner() {alert("Class2");} |
member of second instance | function inner() {alert("Class2");} |
two members equal? | false |
two members strict equal? | false |
function Class3() {} | |
member of function prototype | function() {alert("Class3");} |
member of first instance | function() {alert("Class3");} |
member of second instance | function() {alert("Class3");} |
two members equal? | true |
two members strict equal? | true |
程序分析:
这里写了3个构造函数Class1、Class2、Class3。
Class1使用前面提到的方法一来给创建的对象添加属性。
Class2与Class1类似,不过添加的属性是指向的一个“事先内部声明”的函数。
Class3则使用前面提到的方法二,通过给构造器的prototype添加属性来给整个类添加属性。
之后分别使用各个构造器构造两个对象,构造的每个对象都有一个名为“member”的属性(此属性也是一个function,在这里我们可以看作这个对象的“方法”)。
分别显示每个对象的“member”字面值;
并使用“相等(==)”“严格相等(===)”运算符来判断:使用同一个构造器构造的不同对象,其“member”是否是相同?
从结果可以看到:
不论是哪个构造器,使用相同构造器构造的不同对象,其“member”属性从字面上来看都是相同的。
但是使用“相等”运算符比较,则得到:
Class1类和Class2类各个对象的“member”属性不同;
Class3类的各个对象“member”属性相同。
原因如下:
在Class1构造器中,是使用“函数表达式”来创建function对象。每调用一次构造器,“函数表达式”就会被执行一遍,也就是说会创建一个新的function对象,所以每个Class1类的对象都有一个不同的“member”属性。
在Class3构造器中,并不会像Class1那样反复创建新的function。所有此类对象的“member”属性都是继承自Class3.prototype,因此是同一个“member”。
再看Class2。原来我曾想,既然像Class1那样写会反复创建function,那么我改成Class2那样的写法:在内部“声明”一个function,然后让“member”成员指向此function,这样一来是否就不会“反复创建”了呢?
结果证明并不是这样,实际是这样的:
任何的function“声明”实际上都是在创建function对象,所以在Class2中仍然是在反复创建function对象。
关于“相等性运算符”
“==”表示普通的“相等”,判断时如果两个操作数类型不同,会先尝试转换数据类型。
“===”表示“严格相等”,判断时不会对数据类型进行转换,因此只要数据类型不一致,结果一定是“不相等”。
与上面对应的还有普通的“不等(!=)”和“严格不等(!==)”。
无论是不是“严格”的相等性运算符,如果两个操作数都是对象,那么只有这两个对象引用相同的对象,或是引用“相互连接的对象”时才算时“相等”。
关于“连接的对象(Joined Objects)”
在JS所实现的规范ECMA262-3第13.1.2节中,对此概念进行了描述,其含义大概是:有两个对象,如果其中一个对象的某个属性发生了变化(如创建、值改变、移除),另一个对象对应的属性也会立即发生相同的变化,那么就称这两个对象是相互连接的。规范还指出对于相互连接的对象,使用相等性运算符比较的结果为“相等”。
也就是说,实际上可以理解为“同一个对象”,因为他们的属性和行为“一摸一样”。
再看函数对象的创建
ECMA262-3中提出了“连接的对象”这个概念,好像是为了专门对函数对象的创建过程来进行补充说明的。
因为在13.2节“创建函数对象(Creating Function Objects)”中,对函数对象的创建过程提供了两种方式:
第一种即是如上面试验的结果那样,完全创建一个新的函数对象。
第二种则是在创建前先判断一下之前是否执行过这段创建函数的代码,如果是则根据之前创建的那个函数对象来创建一个“连接的对象”,否则创建一个新的函数对象,这种方式更为“优化”一些。
在第二种方式中,你甚至可以认为没有创建新的函数对象,而是在继续使用原来创建的对象,因为“连接的对象”是“一摸一样”的,使用“===”都会的到“true”的结果!
问题是,ECMA262-3虽然提供了这两种实现途径,但并未强加限制,具体由实现环境自己处理。
所以我之前的“误会”还是有点道理的,连ECMA262-3都这么写了。
可惜的是根据前面的试验结果,IE和FF应该都是使用的第一种实现方式。
结论:
请尽量不要在会被多次调用的函数中创建“局部函数”,因为那样可能可能会在函数多次调用后创建多个“局部函数”对象,可能影响到程序的性能。
推荐使用上面Class3的方式给类添加可供继承的属性,这在ECMA262-3中是没有歧义的,而且绝对比Class1的方式开销要小。
PS:我为什么在试验的window.onload()方法中创建了一个局部函数addRow()?
因为window.onload()只会一次加载运行,写脚本代码还是要图个方便嘛,而且这样我还可以省掉一个参数“oTable”呢……
虽然对于JS来说,这点效率问题不能算是问题,但在如今AJAX日益流行形势下,说不定能起点作用呢。
参考
ECMAScript Language Specification Edition 3
http://www.ecma-international.org/publications/files/ecma-st/ECMA-262.pdf
http://www.mozilla.org/js/language/E262-3.pdf
两本内容好像差不多,下面的这个好像新一点。