您的位置:首页 > Web前端 > JavaScript

javascript面向对象之我解

2008-01-06 22:53 169 查看
今天要用js实现一些客户端功能,考虑到业务逻辑,使用OO的开发方式会很方便,于是认真查看了相关的几篇文章,有一些心得体会。

首先是定义类。js中定义类是使用function,实例化使用new操作符:

var c1 = new class1();
在js中,Function和Object是两个最基础的类,js中的任何对象、实例、函数都同时是Function和Object的实例,这可以用instanceof来验证,这一点有特殊的作用,后面的一些魔法全靠这一点支撑。

function有两种用法,一种如上,是常用的形式;另一种是运行时动态创建,如
var add = new Function("x", "y", "return (x+y)");
创建的结果是一个Function对象。这可以解释我之前看到的“奇怪”的用法。如上,this.m1 = function() {...}的用法中,实际上后面的function句自动产生了一个匿名的function对象,然后赋值给m1属性,随即class1就具有了m1方法。之后又用class1.prototype.m2 = function(){...}句定义了m2方法,实际上也是得到了匿名的function对象,然后赋值给m2属性。当然,这两种定义方法的形式是由区别的,后面会讲到。既然得到的是一个对象,可以使用变量指代,当然就可以作为函数调用的参数了,也就可以轻松而自然的实现函数回调了。这比C#之类的语言实现回调更方便自然。

这里再多啰嗦一句,js类在定义的时候可以定义属性,如示例中的a属性。除此之外,实例的属性是可以动态添加的。如果使用c1.b = 'b',则c1自动具有了属性b。这种动态添加的属性是不会扩散到其他实例的,即如果有另一个实例c2,则c2仍然只有属性a而无属性b。

如上示例,任何类(class1)的函数有两种实现方式,一种的内联的方式,即在定义function的时候,将方法写在body中;另一种是使用prototype,可以在任何地方定义。对于类的实例(class1的实例c1),同时拥有这两种方式定义的方法。因为内联定义的优先级高,在实例调用方法时,首先查找内联定义,然后转到prototype的定义中查找。因此,prototype定义的方法会被内联定义覆盖掉。如果真的发生了方法覆盖,要想使用prototype定义的方法,至少有两种方法(以下假设class1中使用同时使用了上述两种形式定义了方法m1)。

第一种是使用delete,首先delete掉m1方法,根据优先级,自然delete掉的是内联的m1方法。什么,不明白为什么还能delete掉方法?首先。js是动态语言;其次,记住方法也只是一个function实例,在class1中只是一个特殊属性而已。
var c1 = new class1();

delete c1.m1; //去掉内联的m1方法

c1.m1(); //调用prototype的m1方法
第二种是使用apply方法。至于apply的使用方法,可以查看js手册。

var c1 = new class1();

class1.prototype.m.apply(c1); //使用prototype中定义的m方法代替c1的m方法

用new实例化的对象仍然同时是Function对象和Object对象,包括这两者的所有实例属性和方法。这里不是很好理解,想想看,我们定义的类同时是Function对象和Object对象的实例,然后实例化后,实例也同时是Function对象和Object对象的实例,有没有一点像父亲和儿子的爸爸是同一个人?呵呵,有点恶心了,还希望高手出来详细剖析一下Function对象和Object对象以及js里面的类型关系。

这里把js和ruby做个比较。js和ruby都有一个默认的全局对象,js中是Global对象,所用定义的全局变量和函数都是Global的成员。从这一点看,js也有一点oo的感觉(至少是形式上):-)。ruby中,同时存在class变量和实例变量,js也是。这种设计是脚本语言的优势,可以在运行时随时改变class的定义,非常灵活和强大。同时两种脚本中的所有类型都不是封闭的,而是开放的,用户可以给语言内置的所有类型添加额外的属性和方法。

另外闲扯一下有趣的话题:js的反射。js中任何对象(function和object)都可以使用如下方式遍历所有成员:
var info = typeof(obj)+'\n';

alert(info);
同样使用反射,可以这样调用方法和属性:
alert(c1['a']);

c1['m1']();
可以这样考虑,在js的类中,本质上不区分方法和属性,因为方法也就是一个特殊的function对象而已。所以,js类中只存在一种东西:属性。可以猜测js类就是一个hash实现,将所有的属性按照名称和值存入。即可以按照一个类对待,也可以简单的作为一个hash对待。当我使用c1[m1]()形式的调用时,实际过程就是取出值,也就是一个function对象,然后执行之。

然后是继承,比较多的人推荐使用prototype实现继承,因为其强大和灵活。这里不详细介绍prototype,仅探讨我的一些思考,需要了解的读者可以参考文后的相关文章。在读者继续阅读之前,需要有以下几个认识:

1、prototype是对象,而不是简单值(数值、字符串等),因此是引用类型。简单的将prototype赋值给另一个变量,实际上只是浅拷贝了一个引用,拥有的是一个对象。既然对prototype的简单赋值是前拷贝,保存的是引用,自然对任何一个引用的修改都会影响所有的引用。

2、对于上文介绍的两种方法定义方式所定义的方法在类的实例中有着不同的处理方式。经过我设计的几个实验和阅读相关文章,认为一个类的实例实际上也是一个prototype,这个prototype包含两部分。一部分是内联定义的方法,prototype中直接保存了这种方法的深拷贝。另一部分是类所定义的prototype,在实例这个prototype中是以一个引用保存。这点很重要,是后面理解prototype继承中的种种陷阱的关键之一;

3、一个类(class1)的prototype就是这个类的一个不完全实例(之前没有相通这点,仔细看看prototype模式,自然就可以明白)。不完全实例是指prototype不包括内联定义的方法和属性。这样,自然可以理解“类的实例实际上也是一个prototype”了。

4、到现在为止,可以看到js中除掉简单类型,所有的类型和实例都是object类型,即便是神秘的prototype也是(其实理解了prototype也只是一个object而已,也就不再神秘了)。所以prototype可以动态增加方法和属性。

笔者是边写边想,边想边写,因此又想到一个有趣的问题,其实在js的实现中就有一个很棒的继承实现,即一个类(class1)是对其prototype(class1.prototype)的继承,子类化的部分就是类定义中的内联部分。所以类的实例有了所有的内联和prototype中的所有方法和属性,而且也有合理的覆写行为--子类可以覆写父类的方法和属性。缺点主要在于类的所有实例都引用了类的prototype,因此对类的prototype的修改会影响到所有实例。其实再想想,这也算不上是缺点,因为我们的思维固化在静态语言的模式中。对于动态语言,语言本身提供了大量不可思议的功能,很多在静态语言中不可能的事情可以实现了,然而陷阱也更多了,很多问题需要语言的使用者依靠编程习惯和技能来避免,对语言使用者的要求更高一些。对于这个所谓的缺点,一个有益的场合是我们确实需要修改所有实例的行为,这时它反过来是一个大大的优点(接下来的实例中会经常看到这种应用)。回到这个问题来,一般而言我们其实不会修改类的prototype的。现实中最有可能产生问题的应该是prototype中定义的object类型的复杂属性,这个在《javascript的prototype继承》有详细讲解。

另一个联想到的问题是微软的Asp.Net Ajax实现中,几乎类的所有方法都是内联定义的。有批评说这样的实现会降低效率,这个我自然同意。但是同时是不是有另外一个优点:如果同样使用了prototype继承法,则这种继承刚好类似于一个类(class1)是对其prototype(class1.prototype)的继承,实现了良好的覆写行为,同时因为基类的所有方法仍然存在于prototype中,对于某些需要调用基类方法的情形也可以满足。不知道微软当时是不是考虑过这个问题,呵呵。

下面列举两种实现方式:

第一种摘自《javascript的prototype继承》:
//类的继承-海浪版

Function.prototype.Extends = function (parentClass)

class2.Extends(class1);

var c1 = new class1();

var c2 = new class2();
这种实现本希望解决原文中提出的四个缺点,但是我认为只是部分实现。

优点:

1、子类的构造函数得以保留,父类的构造函数可以在子类构造时调用了;

2、父类中内联定义的方法和属性在子类中也以内联的方式出现;

3、得益于2的实现,如果在父类中内联定义了object复杂属性,由于子类中依然是内联属性,不会出现prototype中的复杂属性副作用。

缺点:

1、构造函数的实现有侵入性,

2、如果需要通过prototype定义方法和属性,则prototype的定义必须在Extends方法调用之后,否则所有的定义将会被Extends方法忽略掉。当然,对原Extends方法稍加改进,就可以消除这个缺点;

2、最主要的一点,虽然通过几个复杂的操作,子类仍然直接保留了父类的prototype。因此,如果基类的另一个子类(假设有的话,但既然使用了继承,一般会有另一个子类)如果使用prototype修改了基类的方法(既然是继承,子类覆写方法的需求是自然而常见的),会同时影响到所有的子类,这实际上意味着子类不能覆写父类的方法。当然,你可以不使用prototype的方式覆写,而使用内联的方式覆写,这时候prototype就不是什么大问题了。

第二种实现摘自《JavaScript类的继承
//为Object类添加静态方法:extend

//通过Object类为每个对象添加方法extend

//定义class1

//定义类class1的成员

//定义class2

//让class2继承于class1并定义新成员

//创建两个实例

var obj1=new class1();

var obj2=new class2();

//试验obj1和obj2的方法

obj1.method();

obj2.method();

obj1.method2();

obj2.method2();
这里和大家讨论一下其中几个“高级”用法。一是prototype={...}的用法。前面说过prototype也只不过是一个对象而已,当然任何一个对象都可以赋值给它。后面的{...}可以理解成定义了一个匿名的类,然后直接实例化成一个对象,再赋值给prototype。二是Object.prototype.extend = function(object) {...}的用法。之前谈到prototype的陷阱时讲过,prototype的这一特性可以动态添加方法,这里就应用了这一特性。这里我有一点不明白,为什么使用return Object.extend.apply(this, [this, object])而不是return Object.extend(this, object)?感觉这两种方式没什么区别,因为Object.extend方法完全没有使用实例内部的任何属性,没有使用this引用。

我对这种实现的看法:

优点:

1、prototype的定义和extend方法可以任意使用,没有调用的次序约束。

2、由于extend方法实现得很巧妙,这种继承方法可以实现"多继承"。我不喜欢多继承,宁愿称其为mixin,因为这和ruby中的mixin很相似。

缺点:

1、不支持父类的构造函数;

2、不合适的方法覆写行为。按照这种设计,父类甚至可以覆写子类的方法。

我想,一个比较完善的js继承应该实现以下特性:

1、子类能够继承所有父类的方法和属性,包括内联定义的和prototype定义的;

2、有合理的覆写行为。子类可以覆写父类的方法和属性,而不是反过来。如果发生覆写,子类仍然应该保存对父类中被覆写的方法和属性的引用,因为子类的实现可能需要调用父类的实现;

3、子类的构造函数中必须调用父类的构造函数,因为父类可能必须参数初始化;

这里给出我的初步实现,还没有深入测试,供参考。

class2.inherit(class1);

function test() {

var c1 = new class2();

alert(c1.constructor);

c1.m();

}

使用的时候需要遵守几个规范:

1、所有类的方法都用内联的方式定义,可以最大限度减少继承的副作用;

2、暂时没有,将来增加。

PS:这篇文章已经写了三天了,边写边想,越想越多,自觉有不少收获。另外,前些日子看了下ruby,把js与ruby作对比,同时加深了我对这两种语言的理解,感觉不错。其他的还没有想到。如果我的任何认识有任何不正确的地方,希望斧正,谢谢!

相关文章:

1、goody9807的《javascript的prototype继承

2、lk的《JavaScript类的继承

3、birdshome的《在JavaScript面向对象编程中使用继承(1)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: