代码改变世界

toString方法无法被继承?

2007-07-17 00:23 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

背景

在我看来,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