老赵点滴


  先做人,再做技术人员,最后做程序员。
  我的理想:“让外国人看中国人写的技术书籍和文章”。Try as I might
posts - 290, comments - 10855, trackbacks - 158, articles - 6
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

toString方法无法被继承?

Posted on 2007-07-17 00:23 Jeffrey Zhao 阅读(6320) 评论(38)  编辑 收藏 所属分类: Ajax & Atlas相关ASP.NET AJAX

背景

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

Feedback

#1楼    回复  引用  查看    

2007-07-17 07:02 by 邹健      
js的代码编写的时候还真不好控制

#2楼    回复  引用  查看    

2007-07-17 09:01 by 网魂小兵      
学习ing。

#3楼 [楼主]   回复  引用  查看    

2007-07-17 09:14 by Jeffrey Zhao      
@邹健
操作DOM可能会有问题,语言本身是非常严谨的。

#4楼    回复  引用  查看    

2007-07-17 09:22 by Anders Liu      
学习... 赵兄太细致了!

#5楼    回复  引用  查看    

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

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

#6楼 [楼主]   回复  引用  查看    

2007-07-17 09:34 by Jeffrey Zhao      
@Anders Liu
这个也是我遇到的问题,只是找了一下原因而已。:)

#7楼 [楼主]   回复  引用  查看    

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

#8楼    回复  引用  查看    

2007-07-17 09:38 by Clark Zheng      
不错,老赵是不是正在研究什么大家伙呀,碰巧发现了子类没有直接继承父类的toString方法? ^_^

#9楼 [楼主]   回复  引用  查看    

2007-07-17 09:49 by Jeffrey Zhao      
@Clark Zheng
没有,toString不是最常用的嘛。

#10楼    回复  引用  查看    

2007-07-17 09:55 by Clark Zheng      
@Jeffrey Zhao
-_-

#11楼    回复  引用  查看    

2007-07-17 10:25 by Go_Rush      
好久没有搞过javascript了,不过在实战中 toString的继承好像用得不是很多吧。
 
其实继承除了用 prototype的 extend大法,还可以用 call,apply之类。
 
<script>
function ClassA() {
    
this.test=function(){
        alert( 
this + "'s test");
    }
    
this.toString=function(){
        
return Object.getType(this);
    }


ClassA.prototype.showme
=function(){
    alert(
this + "'s showme");


Object.getType
=function(obj){
    
if (obj.constructor===ClassA) return "this is classAAA";
    
if (obj.constructor===ClassB) return "this is classBBB";
}

Object.extend
=function(src,dest){
    
for(var key in src) dest[key]=src[key];
}

//Object.extend(ClassA,ClassB);
Object.extend(ClassA.prototype,ClassB.prototype);

function ClassB(){
    ClassA.call(
this);
}

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

alert(a);
alert(b);

a.test();
b.test();

a.showme();
b.showme();
</script>

toString也不一定要写在 prototype链上面呀

#12楼    回复  引用  查看    

2007-07-17 10:29 by Go_Rush      
为了更好的设计 ClassB,
在运行时 call 一下,和上面的效果是一样的.


function ClassB(){}

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

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

#13楼 [楼主]   回复  引用  查看    

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

#14楼 [楼主]   回复  引用  查看    

2007-07-17 10:36 by Jeffrey Zhao      
@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的。

#15楼    回复  引用  查看    

2007-07-17 10:36 by Leepy      
如果要继承toString有什么好处么?我可以直接在Demo.Child.prototype里面写toString来定义自己的方法啊

#16楼 [楼主]   回复  引用  查看    

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

#17楼    回复  引用  查看    

2007-07-17 10:51 by Go_Rush      
@Jeffrey Zhao

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

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

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

#18楼 [楼主]   回复  引用  查看    

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


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

#19楼    回复  引用    

2007-07-17 14:23 by 化石 [未注册用户]
注:每个Object对象都包含constructor属性、toString方法和valueOf方法,
每个Function对象包含prototype属性。
除toString方法可被MF使用for...each...枚举外,其他成员都不可被枚举。

#20楼 [楼主]   回复  引用  查看    

2007-07-17 15:01 by Jeffrey Zhao      
@化石
MF是什么啊?

#21楼    回复  引用  查看    

2007-07-17 15:34 by birdshome      
这个toString方法在我写的“类”里面,都是用来返回类的名字的,比如:

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

...

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

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

#22楼 [楼主]   回复  引用  查看    

2007-07-17 16:28 by Jeffrey Zhao      
@birdshome
呵呵,我都是比如输出this.Name + ", " + this.Salary的。:)

#23楼    回复  引用    

2007-07-17 19:40 by 化石 [未注册用户]
@Jeffrey Zhao
MF是Mozilla Firefox的简写。在firefox里,MS Ajax可以实现toString的继承,IE里不可以。

#24楼    回复  引用    

2007-07-17 19:58 by 怪怪 [未注册用户]
好文..

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

#25楼 [楼主]   回复  引用  查看    

2007-07-17 21:05 by Jeffrey Zhao      
@化石
可以吗?我尝试了一下还是不行啊。

#26楼 [楼主]   回复  引用  查看    

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

#27楼    回复  引用  查看    

2007-07-18 09:22 by 淡泊江湖      
good,路过……

#28楼    回复  引用    

2007-07-20 17:17 by Gram [未注册用户]
請問,這些新的Javascript寫法那裏有資料,如:$get等,不知道在那裡可以找到?

#29楼    回复  引用    

2007-07-20 17:20 by Gram [未注册用户]
UpdatePanel能不能向後台提交一個自己給定的參數,如:Ajax的open方法中的那個URL裡面含有的參數字串串?

#30楼 [楼主]   回复  引用  查看    

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

#31楼    回复  引用  查看    

2007-08-03 11:32 by 房客      
老赵好样的.我对于你的态度非常欣赏`!基本上每一个留言你都很耐心地回复了.

#32楼    回复  引用  查看    

2007-08-03 11:33 by 房客      
  先做人,再做技术人员,最后做程序员。
  我的理想:“让外国人看中国人写的技术书籍和文章”。Try as I might

so Good``

#33楼 [楼主]   回复  引用  查看    

2007-08-03 13:34 by Jeffrey Zhao      
@房客
谢谢:)

#34楼    回复  引用    

2007-08-03 17:05 by hflkl1314 [未注册用户]
@Jeffrey Zhao
喜欢老赵的课 支持

#35楼    回复  引用    

2007-08-29 21:37 by 梧桐雨 [未注册用户]
学习!

#36楼    回复  引用  查看    

2007-10-22 13:09 by 坐断东南 笑煞之!!      
学习了!!

#37楼    回复  引用    

2007-11-23 17:01 by 包装机 [未注册用户]
老赵的课 支持

#38楼    回复  引用    

2007-11-23 17:02 by 物资回收 [未注册用户]
哈,很好

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
"五向定位"职业成长路线公开课(上海、南京、大连)
Google站内搜索


相关链接: