Hello World
Spiga

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 
Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

38 条回复

  1. 邹健
    *.*.*.*
    链接

    邹健 2007-07-17 07:02:00

    js的代码编写的时候还真不好控制

  2. 网魂小兵
    *.*.*.*
    链接

    网魂小兵 2007-07-17 09:01:00

    学习ing。

  3. 老赵
    admin
    链接

    老赵 2007-07-17 09:14:00

    @邹健
    操作DOM可能会有问题,语言本身是非常严谨的。

  4. Anders Liu
    *.*.*.*
    链接

    Anders Liu 2007-07-17 09:22:00

    学习... 赵兄太细致了!

  5. 木野狐
    *.*.*.*
    链接

    木野狐 2007-07-17 09:26:00

    不错,这个是强制拷贝了另外一些指定名称的属性啊。
    看来 js 模拟 oo 还是有点累。不过 js 总体的灵活性和动态性我还是满意的。

    不知道老赵对 JScript.net 有研究么?在 asp.net futures release 里面有的,和 IronPython 并列的一个动态语言,没时间去试验。

  6. 老赵
    admin
    链接

    老赵 2007-07-17 09:34:00

    @Anders Liu
    这个也是我遇到的问题,只是找了一下原因而已。:)

  7. 老赵
    admin
    链接

    老赵 2007-07-17 09:35:00

    @木野狐
    我没有研究过JScript.net,而且我觉得这个东西应该不太容易被接受。一般还是IronPython和IronRuby会比较普及吧。

  8. Clark Zheng
    *.*.*.*
    链接

    Clark Zheng 2007-07-17 09:38:00

    不错,老赵是不是正在研究什么大家伙呀,碰巧发现了子类没有直接继承父类的toString方法? ^_^

  9. 老赵
    admin
    链接

    老赵 2007-07-17 09:49:00

    @Clark Zheng
    没有,toString不是最常用的嘛。

  10. Clark Zheng
    *.*.*.*
    链接

    Clark Zheng 2007-07-17 09:55:00

    @Jeffrey Zhao
    -_-

  11. Go_Rush
    *.*.*.*
    链接

    Go_Rush 2007-07-17 10:25:00

    好久没有搞过javascript了,不过在实战中 toString的继承好像用得不是很多吧。
     
    其实继承除了用 prototype的 extend大法,还可以用 call,apply之类。
     
    <

  12. Go_Rush
    *.*.*.*
    链接

    Go_Rush 2007-07-17 10:29:00

    为了更好的设计 ClassB,
    在运行时 call 一下,和上面的效果是一样的.


    function ClassB(){}

    var a=new ClassA();
    var b=new ClassB();

    ClassA.call(b);
    .....
    b.somemethod()
    .....

  13. 老赵
    admin
    链接

    老赵 2007-07-17 10:33:00

    @Go_Rush
    我倒觉得toString还是很常用的,呵呵。
    在JavaScript里实现继承一般还是用prototype的,因为需要在子类的实现中调用方法,总不见的用parentObj.xxx.call(childObj)吧……而且其实你的例子中Object.extend(ClassA.prototype,ClassB.prototype)还是在把A.prototype的东西复制到B上去阿。
    ClassA.call(this)的作用只是相当于“构造子类时调用父类构造函数”而已……

  14. Leepy
    *.*.*.*
    链接

    Leepy 2007-07-17 10:36:00

    如果要继承toString有什么好处么?我可以直接在Demo.Child.prototype里面写toString来定义自己的方法啊

  15. 老赵
    admin
    链接

    老赵 2007-07-17 10:36:00

    @Go_Rush
    ClassA.call(b)能够生效,是因为你写了这样的代码:
    function ClassA() {
    this.test=function(){
    alert( this + "'s test");
    }
    this.toString=function(){
    return Object.getType(this);
    }
    }
    这么做效率不高,因为每次构造ClassA实例时都需要为test和toString复值。所以一般继承还是基于prototype的。

  16. 老赵
    admin
    链接

    老赵 2007-07-17 10:39:00

    @Leepy
    可以通过Parent定义toString,然后它的所有子类只要专注于自己的逻辑就可以了,它们可以有统一的表现线形式。这么做就利用了类似于Template Method模式。

  17. Go_Rush
    *.*.*.*
    链接

    Go_Rush 2007-07-17 10:51:00

    @Jeffrey Zhao

    呵呵,你可以误会我的意思了。
    和你一样,我也十分推荐用extend prototype的方法来实现继承。
    但是 toString, toLocaleString, valueOf,
    hasOwnProperty, isPrototypeOf, propertyIsEnumerable
    这些是在prototype无法枚举出来的。

    如果我们要实现以上这些方法,并打算将来用于子类继承的话。
    用 this.toString=function(){..} 可能更直观哦。
    效率应该不是问题,因为即使上面所有的方法都实现的话,也只有六个。

    我们是为了解决特定的问题才这样写的嘛, 并不是说把所有方法都写在类里面

  18. 老赵
    admin
    链接

    老赵 2007-07-17 11:47:00

    @Go_Rush
    嗯,的确。如果直接用下面的做法也能得到效果的,而且也不需要像我这样修改一个方法了:
    Demo.Parent = function()
    {
    this.toString = function()
    {
    ...
    }
    }


    我的做法相当于保持了MS AJAX Lib的使用方法不变,然后解决了这个问题吧,呵呵。:)

  19. 化石[未注册用户]
    *.*.*.*
    链接

    化石[未注册用户] 2007-07-17 14:23:00

    注:每个Object对象都包含constructor属性、toString方法和valueOf方法,
    每个Function对象包含prototype属性。
    除toString方法可被MF使用for...each...枚举外,其他成员都不可被枚举。

  20. 老赵
    admin
    链接

    老赵 2007-07-17 15:01:00

    @化石
    MF是什么啊?

  21. birdshome
    *.*.*.*
    链接

    birdshome 2007-07-17 15:34:00

    这个toString方法在我写的“类”里面,都是用来返回类的名字的,比如:

    ...
    this.toString = function()
    {
        return '[class Menu]';
    };

    ...

    从来没有用它来输出有运算意义的结果或值:)

    http://birdshome.cnblogs.com/archive/2005/04/14/115851.html

  22. 老赵
    admin
    链接

    老赵 2007-07-17 16:28:00

    @birdshome
    呵呵,我都是比如输出this.Name + ", " + this.Salary的。:)

  23. 化石[未注册用户]
    *.*.*.*
    链接

    化石[未注册用户] 2007-07-17 19:40:00

    @Jeffrey Zhao
    MF是Mozilla Firefox的简写。在firefox里,MS Ajax可以实现toString的继承,IE里不可以。

  24. 怪怪[未注册用户]
    *.*.*.*
    链接

    怪怪[未注册用户] 2007-07-17 19:58:00

    好文..

    不过我更同意Go_Rush的做法, 因为封装一个细节, 有时候会破坏一个契约, 当这个细节产生于比一个框架更基础的东西的时候. 框架我倾向于只封装自己的逻辑. 如果用Go_Rush的做法, 其它程序员一看就知道你要干嘛, 或者说他不知道(比如我这个半吊子就好多问题搞不清楚), 一查就知道你为什么这么干. 但如果其它程序员正好知道这件事, 但他不知道框架被修改了, 那么他还会用自己的方式去做, 这样相当于白做工了. 而且如果他本身预期的结果, 是JS本身的结果, 反而会产生预料之外的事, 他还得去琢磨. 当然在toString上没这么严重啦.

  25. 老赵
    admin
    链接

    老赵 2007-07-17 21:05:00

    @化石
    可以吗?我尝试了一下还是不行啊。

  26. 老赵
    admin
    链接

    老赵 2007-07-17 21:07:00

    @怪怪
    嗯,的确也有这样的问题……不过我的出发点是给出一个底层框架,使开发人员得到统一的体验。上次我为IE做了一个假的XMLHttpRequest也是这么想的。

  27. 淡泊江湖
    *.*.*.*
    链接

    淡泊江湖 2007-07-18 09:22:00

    good,路过……

  28. Gram[未注册用户]
    *.*.*.*
    链接

    Gram[未注册用户] 2007-07-20 17:17:00

    請問,這些新的Javascript寫法那裏有資料,如:$get等,不知道在那裡可以找到?

  29. Gram[未注册用户]
    *.*.*.*
    链接

    Gram[未注册用户] 2007-07-20 17:20:00

    UpdatePanel能不能向後台提交一個自己給定的參數,如:Ajax的open方法中的那個URL裡面含有的參數字串串?

  30. 老赵
    admin
    链接

    老赵 2007-07-20 17:49:00

    @Gram
    1、http://ajax.asp.net/docs
    2、您可以在页面上放置一个<input type="hidden" />,然后再服务器端获取这个值。

  31. 房客
    *.*.*.*
    链接

    房客 2007-08-03 11:32:00

    老赵好样的.我对于你的态度非常欣赏`!基本上每一个留言你都很耐心地回复了.

  32. 房客
    *.*.*.*
    链接

    房客 2007-08-03 11:33:00

      先做人,再做技术人员,最后做程序员。
      我的理想:“让外国人看中国人写的技术书籍和文章”。Try as I might

    so Good``

  33. 老赵
    admin
    链接

    老赵 2007-08-03 13:34:00

    @房客
    谢谢:)

  34. hflkl1314[未注册用户]
    *.*.*.*
    链接

    hflkl1314[未注册用户] 2007-08-03 17:05:00

    @Jeffrey Zhao
    喜欢老赵的课 支持

  35. 梧桐雨[未注册用户]
    *.*.*.*
    链接

    梧桐雨[未注册用户] 2007-08-29 21:37:00

    学习!

  36. 坐断东南 笑煞之!!
    *.*.*.*
    链接

    坐断东南 笑煞之!! 2007-10-22 13:09:00

    学习了!!

  37. 包装机[未注册用户]
    *.*.*.*
    链接

    包装机[未注册用户] 2007-11-23 17:01:00

    老赵的课 支持

  38. 物资回收[未注册用户]
    *.*.*.*
    链接

    物资回收[未注册用户] 2007-11-23 17:02:00

    哈,很好

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我