JavaScript权威指南--类和模块
知识要点
每个javascript对象都是一个属性集合,相互之间没有任何联系。在javascript中也可以定义对象的类,让每个对象都共享某些属性,这种“共享”的特性是非常有用的。类的成员或实例都包含一些属性,用以存放它们的状态,其中有些属性定义了它们的行为(通常称为方法)。这些行为通常是由类定义的,而且为所有实例所共享。例如,假如有一个名为complex的类用来表示复数,同时还定义了一些复数运算。一个complex实例应当包含复数的实部和虚部,同样Complex还会定义复数的加法和乘法操作(行为)。
在javascript中,类的实现是基于其原型继承机制的。如果两个实例都从一个原型对象上继承了属性,我们说它们是同一个类的实例。
如果两个对象继承自同一个原型,往往意味着(但不是绝对)它们是由同一个构造函数创建并初始化的。
如果你对java和c++这种强类型(强、弱类型是指类型检查的严格程度,为所有变量指定数据类型称为强类型)比较熟悉,你会发现javascript中的类和java及c++的类型有很大不同。尽管在写法上类似,而且在javascript中也能“模拟”出很大经典的特性(比如传统类的封装、继承和多态)但是最好要理解javascript类和基于原型的继承机制,以及和传统的java(当然还有类似java的语言)的类和基于类的继承机制的不同之处。
javascript中类的一个重要特征是“动态可继承”(dynamically extendable)。
1.类与原型
在javascript中,类的所有实例对象都从一个类型对象上继承属性。因此,原型对象是类的核心。
如果定义了一个原型对象,然后通过inherit()函数创建了一个继承自它的对象,这样就定义了一个javascript类。
通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象。参照下面例子:
//一个简单的javascript类 //实现一个能表示值的范围的类 //inherit函数 //inherit()返回了一个继承自原型对象p属性的新对象 //这里是有ECMAScript5中的Object.create()函数(如果存在的话) //如果不存在Object.create,则使用其他方法 function inherit(p) { if (p == null) throw TypeError(); //p是一个对象,不能是null if (Object.create) //如果Object.create存在 return Object.create(p); //直接使用它 var t = typeof p; //否则进一步检测 if (t !== "object" && t !== "function") throw TypeError; function f() {}; //定义一个空构造函数 f.prototype = p; //将其原型属性设置p return new f(); //将f()创建p的继承对象 } //inherit函数结束 //这个工厂方法返回一个新的“范围对象” function range(from,to){ //使用inherit()函数来创建对象,这个对象继承自下面定义的原型对象 //原型对象作为函数的一个属性存储,并定义所有“范围对象”所共享的方法(行为) var r = inherit(range.methods); //储存新的“范围对象”启始位置和结束位置(状态) //这两个属性是不可继承的,每个对象都拥有唯一的属性 r.from = from; r.to = to; //返回这个新创建的对象 return r; } //原型对象定义方法,这些方法为每个范围对象所继承 range.methods = { //如果x在范围内,则返回true;否则返回false //如果这个方法可以比较数字范围。也可以比较字符串和日期范围 includes:function(x){ return this.from <= x && x <= this.to;}, //对于范围内每个整数都调用一次f //这个方法只可用作数字范围 foreach:function (f){ for (var x = Math.ceil(this.from); x <= this.to ; x++) f(x); }, //返回表示这个范围的字符串 toString:function(){return "("+ this.from + "..." + this.to + ")";} }; //这是使用范围对象的一些例子 var r =range(1,3); //创建一个范围对象 console.log(r.includes(2)); //true:2 在这个范围内 console.log(r.foreach(console.log)); //输出1,2,3 console.log(r)
这段代码定义了一个工厂方法range(),用来创建新的范围对象。我们注意到,这里给range()函数定义了一个属性range.methods,用以便捷地存放定义类的原型对象。把原型对象挂载函数上没什么大不了,但也不是惯用做法。再者,注意range()函数给每个范围对象定义了from和to属性,用以定义范围的起始位置和结束位置,这两个属性是非共享的、可继承的方法都用到了form和to属性,而且使用了this关键字,为了指代它们,二者使用的this关键字来指代调运这个方法的对象。任何类的方法都可以通过this的这种基本用法来读取对象的属性。
2.类和构造函数
上边的例子中展示了javascript中定义类的其中一种方法。但这种方法并不常用,毕竟它没有定义构造函数,构造函数是用来初始化和创建对象的。
8章讲到的使用new关键字来调用构造函数,使用new调用构造函数会创建一个新对象,因此,构造函数本身只需要初始化这个新对象的状态即可。调用构造函数的一个重要特征是,构造函数的prototype属性被用做新对象的原型。这意味着通过同一个构造函数创建的对象都是继承自一个相同的对象,因此它们都是一个类的成员。下面的例子对上面的例子的“范围类”做了修改,使用构造函数代替工厂函数:
//表示值的范围的类的另一种实现 //这是一个构造函数,用以初始化新创建的“范围对象” //注意,这里并没有创建并返回一个对象,仅仅是初始化 function Range(from, to) { //存储这个“范围对象”的起始位置和结束位置(状态) //这两个属性是不可继承的,每个对象都拥有唯一的属性 this.from = from; this.to = to; } //所有的“范围对象”都继承自这个对象 //属性的名字必须是"prototype" Range.prototype = { //如果x在范围内,则返回true;否则返回false //这个方法可以比较数字范围,也可以比较字符串和日期范围 includes: function(x) { return this.from <= x && x <= this.to; }, //对于这个范围内的每个整数都调用一次f //这个方法只可用于数字范围 foreach: function(f) { for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); }, //返回表示这个范围的字符串 toString: function() { return "(" + this.from + "..." + this.to + ")"; } }; //这里是使用“范围对象”的一些例子 var r = new Range(1, 3); //创建一个范围对象 r.includes(2); //=>true 2在这个范围 内 r.foreach(console.log); //输出1 2 3 console.log(r); //输出对象(1...3)
这里遵循了一个常见的编程约定:从某种意义上来讲,定义构造函数既是定义类,并且类首字母要大写,而普通的函数和方法首字母都是小写。
再者,注意Rang()构造函数是通过new关键字调用的,而range()工厂函数则不必使用new。前者调用普通函数创建对象,后者则使用构造函数。
由于Rang()函数就是通过new关键字来调用的,所有不必调用inherit()或者其它什么的逻辑来创建新的对象。在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象。Range()构造函数只不过是初始化this而已。构造函数甚至不必返回这个新创建的对象,构造函数会自动创建对象,然后将构造函数作为这个对象的方法来调用一次,最后返回这个新对象。
事实上,构造函数的命名规则和普通函数是如此不同还有另外一个原因,构造函数调用和普通函数的调用是不尽相同的。构造函数就是用来“构造新对象”的,它必须通过关键字new来调用,如果将构造函数做普通函数的话,往往不会正常工作。开发者可以通过命名约定来判断是否应当在函数之前冠以关键字new。
两个例子还有一个非常重要的区别:就是原型对象的命名。在第一段示例代码中的原型是range.methods。这种命名方式很方便同时具有很好的语义,但有过于随意。在第二段代码中的原型是Rang.prototype,这是一个强制命名。对Range()构造函数的调用会自动使用Rang.prototype作为新Range对象的原型。
最后,需要注意的是两个例子中两种类定义方法的相同之处,两者的范围方法定义和调用方式是完全一致的。
2.1.构造函数和类的标识
上文提到,原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们在属于同一个类的实例。而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象。那么这两个构造函数创建的实例是属于一个类的。
尽管构造函数不像原型那样基础,但构造函数是类的“外在表现”。 很明显,构造函数的名字通常用做类名。比如,我们说Rang()构造函数创建Range对象,然后根本的讲,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。假设这里有一个对象r,我们想知道r是否是Range对象,我们来这样写:
r instanceof Range // 如果r继承自Rang.prototype,则返回true
实际上instanceof运算符不不会检查r是否是由Range()构造函数初始化而来,而会检查r是否继承Range.prototype。不过instanceof的语法强化了“构造函数是类公有标识”的概念,在本章的后面还会碰到对instanceof运算符的介绍。
2.2.constructor属性
在上面的例子中,将Range.prototype定义为一个新对象,这个对象包含类所需要的方法。其实没必要新创建一个对象,用单个对象的直接量的属性就可以方便地定义原型上的方法。任何javascript函数都可以用做构造函数,并且调用构造函数是需要用到一个prototype属性 ,因此,每个javascript函数(ECMAScript5中的function.bind()方法返回的函数除外)都自动拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举的属性constructor属性的值是一个函数对象:
var F = function() {}; //这是一个函数对象: var p = F.prototype; //这是F相关联的原型对象 var c = p.constructor; //这是与原型相关的函数 c === F; //=>true 对于任意函数F.prototype.constructor == F
可以看到构造函数的原型中存在预先定义好的constructor属性,这意味着对象通常继承的constructor均代指他们的构造函数。由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类。
var o = new F(); //创建类F的一个对象 o.constructor === F //=>true ,constructor属性指代这个类
如下图所示,展示了构造函数和原型之间的关系,包括原型到构造函数的反向引用及构造函数创建的实例。
需要注意的是,上图使用早前的Range()构造函数作为示例,但实际上,例子中定义的Range类使用它自身的一个新对象重写了预定义的Range.prototype对象。这个新定义的原型对象不含有constructor属性。因此Range类的实例也不包含有constructor属性。我们可以通过补救措施来修正这个问题,显式的给原型添加一个构造函数:
Range.prototype = { constructor: Range, //显式的设置构造函数反向引用 includes: function(x) {return this.from <= x && x <= this.to;}, foreach: function(f) {for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);}, toString: function() {return "(" + this.from + "..." + this.to + ")";} };
另外一种常见的解决办法是使用预定义的原型对象,预定义的原型对象包含constructor属性,然后依次给原型对象添加方法:
//扩展预定义的Range.prototype对象,而不重写之 //这样就自动创建Range.prototype.constructor属性 Range.prototype.includes = function(x) { return this.from <= x && x <= this.to; }; Range.prototype.foreach = function(f) { for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); } ange.prototype.toString: function() { return "(" + this.from + "..." + this.to + ")"; }
3.javascript中java式的类继承
如果你有过Java或者其它类似强类型面向对象语言开发的经历的话,在你的 脑海中,类成员的模样可能是这个样式
实例字段:
它们是基于实例的属性或变量,用以保存独立对象的状态
实例方法:
它们是类的所有实例所共享的方法,有每个独立的实例调用
类字段:
这些属性或变量是属于类的,而不属于类的某个实例
类方法
这些方法是属于类的,而不是属于类的某个实例的。
javascript和java的一个不同之处在于,javascript中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别,如果属性值是函数,那么这个属性就定义一个方法;否则,它是一个普通的属性或“字段”。尽管存在诸多差异,我们还是可以用javascript模拟出java中的这四种类成员类型。javascript中的类牵扯三种不同的对象(参照上图),三种对象的属性的行为和下面三种类成员非常相似:
构造函数对象:之前提到,构造函数(对象)为javascript的类定义了名字。任何添加到这个构造函数对象中的属性都是类字段和类方法(如果属性值是 函数的话就是离方法)
原型对象:原型对象的属性被类的所有实例所继承,如果原型对象的属性值是函数的话,这个函数就作为类的实例方法来调用
实例对象:类的每个实例对象都是一个独立的对象,直接 给这个实例定义的属性是不会为所有实例对象锁共享的。定义在实例上的非函数属性,实际上是实例的字段。
在javascript中定义类的步奏可以缩减为一个分三步的算法。第一步,先定义一个构造 函数,并设置初始化新对象的实例属性。第二步,给构造函数的prototype对象定义实例的方法。第三步:给构造函数定义类字段和类属性。我们可以将这三个步奏封装进一个简单的defineClass()函数中(这里用到了6和8章的extend()函数):
///一个用以简单类的函数 function defineClass(constructor, //用以设置实例的属性的函数 methods, //实例的方法,复制至原型中 statics) //类属性,复制至构造函数中 { if (methods) extend(constructor.prototype, methods); if (statics) extend(constructor, statics); return constructor; } //这个是Rang类的另一个实现 var SimpleRange = defineClass(function(f, t) {this.f = f;this.t = t;}, { inclueds: function(x) {return this.f <= x && x <= this.t;}, toString: function() {return this.f + "..." + this.t;} }, {upto: function(t) {return new SimpleRange(o, t);}});
下面的定义类的代码更长一些,这里定义了一个表示复数的类,这段代码展示了如何使用javascript来模拟实现java式的类成员,这里的代码没有用到上面的defineClass()函数,而是“手动”实现:使用到了构造函数、实例字段、实例方法、类字段和类方法。
。。。。。。
尽管javascript可以模拟出java式的类成员,但java中很多重要的特性是无法在javascript类中模拟的。首先对于java类的实例方法来说,示例字段可以做局部变量,而不需要使用关键字this来引导他们。javascript是无法模拟这个特性的。但可以使用with语句来近似的模拟实现这个功能(不推荐)
。。。。。。
4.类的扩充
javascript中基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象之后原型的属性发生改变,也会影响到继承这个原型的所有实例对象。这意味着我们可以通过给原型对象添加新的方法来扩充javascript类。
javascript内置类的原型对象也是一样的如此“开放”,也就是说可以给数字,字符串、数组、函数等数据类型添加方法。
if(!Function.prototype.bind){ function.prototype.bind = function(o/*,agrs*/){ //bind()代码的方法... }; }
其他:
//多次调用这个函数f,传入一个迭代鼠 //比如输出三次"hello" //var n =3 //n.times(function(n){console.log(n + "hello");}); Number.prototype.times = function(f,context){ var n = Number(this); for(var i = 0;i< n;i++) f.call(context,i); }; //如果不存在ECMAscript5的String.trim()方法的话,就定义它 //这个方法用于去除字符串开头和结尾的空格 String.prototype.trim = String.prototype.trim || function(){ if (!this) return this; return this.replace(/^\s|\S+$/g,""); //使用正则表达式 进行空格替换 }; //返回函数的名字,如果它有(非标准的)name属性,则直接使用name属性 //否则,将函数转换为字符串然后从中提取名字 //如果是没有名字的函数,则返回一个空字符串 Function.prototype.getName = function(){ return this.name || this.toString().match(/function\s*([^()*]\(/)[1]; };
可以给Object.prototype添加方法,从而使所有的对象都可以调用这些方法。但这样的做法并不推荐,因为在ECMAScript5之前,无法将这些新增的方法设置为不可 枚举的,如果给Object.prototype添加属性,这些属性是可以被for/in循环遍历到的。在9.8.1将给出ECMAScript5的一个例子,其中使用Object.defineProperty()方法安全地扩充Object.prototype。
然而并不是所有的宿主环境(比如web浏览器)都可以使用Object.define(),这跟ECMAScript具体实现有关。比如在很多web浏览器里,可以给HTMLElement.prototype添加方法,这样当前文档中表示HTML标记的所有对象就可以继承这些方法,但IE有可能不支持这样做。这对客户端编程实用技术有严重的限制。
5.类和类型
我们往往更希望将类作为类型来对待,这样就可以根据对象所属的类来区分它们。javascript语言核心中的内置对象(通常是指客户端javascript的宿主对象)可以根据它们的class属性(6.8.2节)来区分彼此,比如用到的classof函数。但当我们使用本章所提到的技术来定义类的话,实例对象的class属性都是"Object",这时calssof()函数也无用武之地。
接下来的几节介绍了三种用以检测任意对象的类的技术:instanceof运算符,constructor属性,以及构造函数的名字。但每种技术都不甚至完美,本节总结讨论了鸭式辨型,这种编程哲学更加注意对象可以完成什么构造(包含什么方法)而不是对象属于那个类。
5.1.instanceof运算符
左操作数是待检测其类的对象,右操作鼠是定义类的构造函数。
如果o继承自c.protoype,则表达式 o intanceof c的值为true。这里的继承可以不是直接继承,如果o锁继承的对象继承自令一个对象,后一个对象继承自c.prototype这个表达式的运算结果也是true。
构造函数是类的公共标识,但原型是唯一的标识。尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。
如果你想检测对象的原型链上是否存在某个特定的原型对象,有没有不使用构造函数作为中介的方法?答案是肯定的,可以使用isPrototypeOf()方法。比如可以通过如下代码来检测对象r是否是例9.1中定义的范围类的成员:
range.methods.isPrototypeOf(r); //range.method是原型对象
instanceof运算符和isPrototypeOf()方法的缺点是,我们无法通过对象来获得类名,只能检测对象是否属于指定的类名。在客户端javascript中还有一个比较严重的不足,就是在多窗口和多框架子页面的web应用中兼容性性不佳。每个窗口和框架子页面都具有段杜的执行上下文,每个上下文都包含独有的全局变量和一组构造函数。在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架Array()构造函数的实例,instanceof运算符结果是false。
5.2.constructor属性
另外一种识别对象是否属于某个类的方法是使用constructor属性。因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,比如:
function typeAndValue (x){ if (x == null) return ""; Null和undefined没有构造函数 switch(x.constructor){ case Number:return "Number:" + x; //处理原始类型 case String:return "String:" + x; case Date: return "Date:" + x; case RegExp:return "RegExp" + x; //处理内置函数类型 case Complex:return "Complex" + x; //处理定义类型 } }
需要注意的是,在代码中关键字case后的表达式都是函数,如果改用typeof运算符或取到对象的class属性的话,它们应当改为字符串。
使用constructor属性检测对象属于某个类的技术不足之处和instanceof一样。在多个执行上下文的场景中它是无法正常的工作(比如在浏览器窗口的多个框架子页面中)。在这样的情况下,每个框架页面各自拥有独立的构造函数集合,一个框架页面中的Array构造函数和另外一个框架页面的Array构造函数不是同一个构造函数。
同样在javascript并非所有的对象都包含constructor属性。在每个新创建的函数上默认会有constructor属性,但我们常常忽略原型上的constructor比如本章前面示例代码中定义的两个类,他们的实例都没有constructor属性。
5.3.构造函数的名称
使用instanceof运算符和constructor属性来检测对象所属的类有一个主要的问题,在多个执行上下文中存在构造函数的多个副本时,这两种方法的检测结果会出错。多个执行上下文的函数懒起来是一模一样的,但它们是相互独立的对象 ,因此彼此也不相等。
一种可能的解决方案是使用构造函数的名字而不是使用构造函数本身作为类标识符。一个窗口里的Array构造函数和令一个窗口的Array构造函数是不相等的,但是它们的名字是一样的。在一些javascript的实现中为了给函数对象提供一个非标准的属性name,用来表示函数的名称。对象那行没有那么属性的javascript实现来说,可以将函数转换为字符串,然后从中提取出函数名(9.4中示例的代码给Function类添加了getName()方法,就是使用这种方法来得到函数名)。
下面定义的type()函数以字符串的方法返回对象的类型。它用typeof运算符来处理原始的值和函数。对于对象来说,它要么返回class属性的值要么返回构造函数的名字。type()函数用到了前面classof()函数和9.4节的Function,getName()方法。为了简单起见,这里包含了函数和方法的代码:
。。。。。。
这种使用函数的名字来识别对象的做法和使用constructor属性一样有一个问题:并不是所有的对象都具有constructor属性。此外,并不是所有的函数都有名字。
5.4.鸭式辨型
上文所描述的检测对象的类的各种技术,多少都有些问题,至少在客户端javascript中是如此。解决的办法就是规避掉这些问题,不要关注“对象的类是什么”,而是关注“对象 的类能做什么”。
对于javascript程序员来说,“如果一个对象可以像鸭子一样走路、游泳并且嘎嘎叫,就认为这个对象就是鸭子,哪怕它并不是从鸭子类的原型对象而继承而来的”。
。。。。。。
6.javascript中的面向对象技术
本节将目标转向一些实际的例子(尽管这不是基础知识),包括如何利用javascript的类进行编辑。
6.1.一个例子:集合类
集合(set)是一种数据结构,用以表示非重复值的无序集合。集合的基础方法包括添加值、检测值是否在集合中,这种集合需要一种通用的实现,以保证操作效率。javascript的对象是属性, 以及对于的值的集合。因此将对象只用作字符串的集合是大材小用。下面的例子用javascript实现了一个更加通用的Set类,它实现了从javascript值到唯一字符串的映射,然后将字符串用作属性名。对象和函数都不具备如此简明可靠的唯一字符串表示。因此,集合类碧玺给集合中的没一个对象或函数定义一个唯一的属性标识。
。。。。。。
6.2.一个例子:枚举类型
枚举类型(enumerated type)是一种类型,它的值是有限集合,如果值定义为这个类型则该值是可以列出(或“可枚举”)的。在C及其派生语言中 ,枚举类是通过关键字enum声明的。enum是ECMAScript5中的保留字(还未使用),很有可能在将来就会内置支持枚举类型。
下面的例子展示了如何在javascript中定义枚举类型的数据。需要注意的是,这里用到了inherit()函数。
下面的第二个例子包含一个单独函数enumeration()。但它不是构造函数,它并没有定义一个名叫“enumeration”的类,相反,它是一个工厂方法,每次调用它都会创建并返回一个新的类。比如:
。。。。。。
6.3.标准转换方法
3.8.3和6.10节讨论了对象类型转化所多用到的重要方法,有一些方法是需要做类型转换时由javascript解释器自动调用的。
最重要的方法首当toString()。这个方法的作用是返回一个表示这个对象的字符串。在希望使用字符串的地方用到对象的话(比如将对象用作属性名或使用“+”运算符来进行字符串的链接运算),javascript会自动调用这个方法,如果没有实现这个方法,类会默认的从Object.prototype中继承toString()方法,这个方法的运算结果是“[object object]”,这个字符串的用处不大。toSring()方法应当返还一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让程序调试变得更加轻松。例如上文中的Range类和Complex类都定义了toString()方法。
toLocaleString()和toString()极为类似:toLocaleString()是以本地敏感性(locale-sensitive)的方式来讲对象转换为字符串。默认情况下,对象所继承的toLocaleString()方法只是简单的调用toString()方法。有一些内置类型包含有用的toLocaleString()方法用以实际上返回本地化相关的字符串,如果需要为对象到字符串的转换定义toString()方法,那么同样需要定义toLocaleString()方法处理本地化的对象到字符串的转换。
第三个方法是valueOf(),它用来将对象转换为原始值。比如,当数学运算符(除了“+”运算符)和关系运算符作用于数字文本表示的对象时,会自动调用valueOf()方法。大多数对象都没有合适的原始值来表示它们,也没有定义这个方法。但在例9-7的枚举类型中实现说明valueOf()方法是非常重要。
第四个方法是toJSON(),这个方法是由JSON.stringify()自动调用的。JSON格式用于序列化良好的数据结构,而且可以处理javascript原始值、数组和纯对象。它和类无关,当对于一个对象执行序列化操作时,将会忽略对象的原型和构造函数。比如将Range对象或Complex对象作为参数传入JSON.stringfy(),将会返回诸如{"form":1,"to":3}或{"r":1,"i":-1}这种字符串。如果将这些字符串传入传入JSON.parse(),则会得到一个和Rangge对象和Complex对象具有相同的属性和纯对象,但这个对象不会包含从Range和Complex继承的方法。
这里使用extend()来向Set.prototype来添加方法:
。。。。。。
6.4.比较方法
javascript的相等运算符比较对象时,比较的是引用而不是值。也就是说,给定两个对象引用,如果要它们是否指向同一个对象,不是检查者两个对象是否具有相同的属性名和相同的属性值,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序(就像"<"和">"运算符进行的比较一样)。
为了能让自定义类的实例具备比较的功能,定义一个名叫equals()实例方法。这个只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true。
当然所说的“相等”的含义是根据类的上下文来决定的。对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同的类型,然后比较两个对象的实例属性以保证它们的值相等。例9-3的Complex类就实现了这一的equals()方法,我们可以轻易地为Range类也实现类似的方法:
。。。。。。
给Set类定义equals()方法稍微有些复杂。不能简单地比较两个集合的values属性,还要进行更深层次的比较:
。。。。。。
按照我们需要的方式比较对象是否相等常常是很有用的。对于某些类来说,往往需要比较一个实例“大于”或者“小于”另外一个示例。
如果将对象用于javascript的关系比较运算,比如“<”和"<=",javascript会首先调用对象的valueof()方法,如果这个方法返回一个原始值,则直接比较原始值。但大多数没有valueOf()方法,为了按照显式定义规则来比较这些类型的对象,可以定义一个叫compareTo()方法。
compareTo()方法只能接收一个参数,这个方法将这个参数和调用它的对象进行比较。如果this对象小于参数对象,如果this对象小于参数对象,compareTo()应当返回比0小的值。如果this对象大于参数对象,应当返回比0大的值。如果两个对象相等,应当返还0。这些官员返回值的约定非常重要,这样我们可以用下面的表达式替换掉关系比较和相等性运算符:
。。。。。。
7.子类
在面向对象编程中,类B可以继承自另外一个类A。我们将A称为父类(superclass),将B称为子类(subclass)。
类B可以定义自己的实例方法,有些方法可以重载A的同名方法,这种做法称为“方法链”(method chaining)。同样,子类的构造函数B()有时需要调用父类的构造函数A(),这种做法称为“构造函数链”(constructor chaining)。子类还可以有子类,当涉及类的层次结构时,往往需要定义抽象类(abstract class)。抽象类中定义的方法没有实现。抽象类中的抽象方法是在抽象类的具体子类中实现的。
7.1.定义子类