toString方法无法被继承?
2007-07-17 00:23 by 老赵, 8128 visits背景
在我看来,toString方法是一个类最重要的方法之一。在JavaScript中,将一个对象转化为字符串形式的默认方法就是调用其toString方法。因此,为类型实现一个合理的toString方法对于开发和调试都有一定的好处。在面向对象编程中,在父类中定义toString方法,以此为它的各个子类提供相似的字符串表现形式是常用的做法之一,但是如果您使用Microsoft AJAX Library的面向对象机制进行开发时就会遇到一个问题。
那就是toString方法无法被继承。
说的更明白一些,就是子类无法获得父类的toString方法的实现。除非在子类中直接定义一个toString方法,否则它只能含有JavaScript中默认的toString方法。很显然,这没有任何意义,也失去了面向对象的重要特性。
问题重现
我们通过一个再简单不过的例子来重现这个问题:
Type.registerNamespace("Demo"); // Definition of Demo.Parent class. Demo.Parent = function() {} Demo.Parent.prototype = { toString : function() { return Object.getTypeName(this); } } Demo.Parent.registerClass("Demo.Parent"); // Definition of Demo.Child class, which inherits Demo.Parent. Demo.Child = function() { Demo.Child.initializeBase(this); } Demo.Child.prototype = {} Demo.Child.registerClass("Demo.Child", Demo.Parent); // Call the toString method implicitly. alert(new Demo.Parent()); alert(new Demo.Child());
上面的代码定义了两个类,父类Demo.Parent和子类Demo.Child。其中父类Demo.Parent中定义了toString方法,因此按照面向对象编程的机制,子类Demo.Child也会使用父类的toString方法实现。可惜结果并不如人意,在IE中,上面的代码会显示如下的结果:
Demo.Parent [object Object]
通过调用Demo.Parent对象的toString方法,我们得到了期望中的表示当前对象实际类型的字符串。但是调用Demo.Child对象的toString方法却只得到了JavaScript中默认的结果。
这是怎么回事?
对于使用JavaScript面向对象机制的实现有一定了解的朋友会知道,JavaScript中是使用了prototype链的特性来实现的面向对象的效果。在Microsoft AJAX Library中,“继承”的做法其实只是遍历父类prototype上的所有属性,并为子类的prototype对象添加不存在的属性。简单地说,它的代码实现就如下面的代码所示(请注意,真正的实现并非只有这部分代码,但是这部分代码是继承实现的关键):
for (var memberName in baseType.prototype) { var memberValue = baseType.prototype[memberName]; if (!this.prototype[memberName]) { this.prototype[memberName] = memberValue; } }
这么做的目的,是希望让子类的prototype对象能够拥有父类的prototype对象中定义的成员,并能够使自身重新定义的方法实现覆盖父类的同名方法。显然,这样就获得了“继承”的效果。不过,如此实现“继承”的重要部分就是使用for...in语法来遍历一个对象上的所有属性——可能有些朋友已经看出问题所在了。没错,我们现在来写一段最简单的代码来验证我们的猜想:
for (var memberName in Demo.Parent.prototype) { alert(memberName); }
果然不出所料,遍历Demo.Parent的prototype对象上的成员却没有得到任何的结果。我们再来写一个更原始的例子,我们直接遍历一个Object对象:
var obj = new Object(); for (var memberName in obj) { alert(memberName); }
toString方法不是每个对象都该有的吗,但是为什么没有遍历出来?其实通过进一步尝试可以发现,与toString方法相似,一些每个对象都有的方法,例如valueOf,hasOwnProperty等等,都无法通过for...in语法来获得。而且,遍历String.prototype对象也无法得到例如split、indexOf等JavaScript定义的方法。这究竟是怎么回事?
答案可以在ECMAScript标准(Ecma-262)中找到。根据标准的描述,JavaScript中的对象是一个无序的属性(Property)集合(属性可以使任何类型,我们传统所说的“方法”其实都是Function类型的对象),而每个属性都拥有有零个或多个特性(Attribute)来“指示”该属性可以被如何使用。例如,一个拥有DontDelete特性的属性就无法从对象里删除。也就是说,以下的操作将没有任何效果:
var array = new Array(); delete array.length;
ECMAScript中为属性定义了4种特性,它们分别是ReadOnly、DontEnum、DontDelete、Internal。很显然,造成对象的toString方法无法被遍历到“元凶”就是DontEnum特性,拥有这个特性的属性将无法通过for...in语法来得到——而似乎JavaScript中的原生属性都有DontEmun特性。
如何解决?
这样的问题必须解决,否则我们的面向对象机制过于“残缺”了。幸好,我们仍旧能够直接从对象上通过名称来直接获取成员。因此我们可以修改Microsoft AJAX Library一个方法实现:
Type.prototype.resolveInheritance = function () { if (this.__basePrototypePending) { var baseType = this.__baseType; baseType.resolveInheritance(); for (var memberName in baseType.prototype) { var memberValue = baseType.prototype[memberName]; if (!this.prototype[memberName]) { this.prototype[memberName] = memberValue; } } var dontEnumMembers = ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable"]; for (var i = 0; i < dontEnumMembers.length; i++) { var memberName = dontEnumMembers[i]; if (this.prototype[memberName] != Object.prototype[memberName]) { continue; } var memberValue = baseType.prototype[memberName]; if (memberValue != Object.prototype[memberName]) { this.prototype[memberName] = memberValue; } } delete this.__basePrototypePending; } }
我不想在这里详细地解释这部分代码,但是请注意我们做了哪些额外的事情。首先我们准备了一个数组dontEnumMemebers,存放了所有定义在Object.prototype对象上的原生属性(它们都是方法),我们如果使用这些名称为自定义的类型定义成员的话,子类将无法继承父类中的这些方法。因此我们会判断在父类中是否使用这些名称定义了方法(通过和Object.prototype对象中的属性进行比较得到这个信息),如果有,则将其复制给子类的prototype对象上。自然,在这之前我们还需要判断子类本身是否定义了该方法,我们不能使用父类的方法来覆盖子类的方法。
重新运行最早的那部分代码,我们现在已经可以得到正确的结果了:
Demo.Parent Demo.Child
注意
虽然我们解决了Microsoft AJAX Library中的继承问题,但是请注意,我们并没有,也无法解决for...in语法无法遍历出toString等成员的问题。例如$create方法会接受多个对象作为存放组件属性,事件以及组件之间相互引用信息的集合。如果这些集合中某一项的key为toString等特定的名称,则可能就会因为无法遍历得到该项而出现错误。不过避免这个问题的方法其实也很简单,只要不使用如下的名称作为key即可:
- toString
- toLocaleString
- valueOf
- hasOwnProperty
- isPrototypeOf
- propertyIsEnumerable
js的代码编写的时候还真不好控制