【类和模块】JavaScript中Java式的类继承
如果你有过Java或其他类似强类型面向对象语言的开发经历的话,在你的脑海中,类成员的模样可能会是这个样子:
实例字段
它们是基于实例的属性或变量,用以保存独立对象的状态。
实例方法
它们是类的所有实例所共享的方法,由每个独立的实例调用。
类字段
这些属性或变量是属于类的,而不是属于类的某个实例的。
类方法
这些方法是属于类的,而不是属于类的某个实例的。
JavaScript和Java的一个不同之处在于,JavaScript中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别。如果属性值是函数,那么这个属性就定义一个方法;否则,它只是一个普通的属性或“字段”。尽管存在诸多差异,我们还是可以用JavaScript模拟出Java中的这四种类成员类型。JavaScript中的类牵扯三种不同的对象(参照 图9-1),三种对象的属性的行为和下面三种类成员非常相似:
构造函数对象
之前提到,构造函数(对象)为JavaScript的类定义了名字。任何添加到这个构造 函数对象中的属性都是类字段和类方法(如果属性值是函数的话就是类方法)。
原型对象
原型对象的属性被类的所有实例所继承,如果原型对象的属性值是函数的话,这个 函数就作为类的实例的方法来调用。
实例对象
类的每个实例都是一个独立的对象,直接给这个实例定义的属性是不会为所有实例 对象所共享的。定义在实例上的非函数属性,实际上是实例的字段。
在JavaScript中定义类的步骤可以缩减为一个分三步的算法。第一步,先定义一个构造函数,并设置初始化新对象的实例属性。第二步,给构造函数的prototype对象定义实例的方法。第三步,给构造函数定义类字段和类属性。我们可以将这三个步骤封装进一个简单的defineClass( )函数中:
// 一个用以定义简单类的函数 function defineClass(constructor, // 用以设置实例的属性的函数 methods, // 实例的方法,复制至原型中 statics) // 类属性,复制至构造函数中 { if(methods) extend(constructor.prototype, methods); if(statics) extend(constructor, statics); return constructor; } // 这是Range类的另一个实现 var SimpleRange defineClass(function(f,t) { this.f = f; this.t = t; }, { includes: function(x) { return this.f <=x && x < this.t;}, toString: function() { return this.f + "..." + this.t; } }, { upto: function(t) { return new SimpleRange(0,t); } });
下例定义类的代码更长一些。这里定义了一个表示复数的类,这段代码展示了如何使用JavaScript来模拟实现Java式的类成员。下例中的代码没有用到上面的defineClass()函数,而是“手动”来实现:
例:Complex.js:表示复数的类 /* Complex.js: * 这个文件定义了Complex类,用来描述复数 * 回忆一下,复数是实数和虚数的和,并且虚数i是-1的平方根 */ /* * 这个构造函数为它所创建的每个实例定义了实例字段r和i * 这两个字段分别保存复数的实部和虚部 * 它们是对象的状态 */ function Complex(real, imaginary) { if (isNaN(real) || isNaN(imaginary)) // 确保两个实参都是数字 throw new TypeError(); // 如果不都是数字则抛出错误 this.r = real; // 复数的实部 this.i = imaginary; // 复数的虚部 } /* * 类的实例方法定义为原型对象的函数值属性 * 这里定义的方法可以被所有实例继承,并为它们提供共享的行为 * 需要注意的是,JavaScript的实例方法必须使用关键字this * 来存取实例的字段 */ // 当前复数对象加上另外一个复数,并返回一个新的计算和值后的复数对象 Complex.prototype.add = function (that) { return new Complex(this.r + that.r, this.i + that.i); }; // 当前复数乘以另外一个复数,并返回一个新的计算乘积之后的复数对象 Complex.prototype.mul = function (that) { return new Complex(this.r * that.r - this.i * that.i, this.r * that.i + this.i * that.r); } // 计算复数的模,复数的模定义为原点(0,0)到复平面的距离 Complex.prototype.mag = function () { return Math.sqrt(this.r * this.r + this.i * this.i); }; // 复数的求负运算 Complex.prototype.neg = function () { return new Complex(-this.r, -this.i); }; // 将复数对象转换为一个字符串 Complex.prototype.toString = function () { return "{" + this.r + "," + this.i + "}"; }; // 检测当前复数对象是否和另外一个复数值相等 Complex.prototype.equals = function (that) { return that != null && // 必须有定义且不能是null that.constructor === Complex && // 并且必须是Complex的实例 this.r === that.r && this.i === that.i; // 并且必须包含相同的值 }; /* * 类字段(比如常量)和类方法直接定义为构造函数的属性 * 需要注意的是,类的方法通常不使用关键字this, * 它们只对其参数进行操作 */ // 这里预定义了一些对复数运算有帮助的类字段 // 它们的命名全都是大写,用以表明它们是常量 //(在ECMAScript 5中,还能设置这些类字段的属性为只读) Complex.ZERO = new Complex(0, 0); Complex.ONE = new Complex(1, 0); Complex.I = new Complex(0, 1); // 这个类方法将由实例对象的toString方法返回的字符串格式解析为一个Complex对象 // 或者抛出一个类型错误异常 Complex.parse = function (s) { try { // 假设解析成功 var m = Complex._format.exec(s); // 利用正则表达式进行匹配 return new Complex(parseFloat(m[1]), parseFloat(m[2])); } catch(x) { // 如果解析失败则抛出异常 throw new TypeError("Can't parse '" + s + "' as a complex number."); }; // 定义类的“私有”字段,这个字段在Complex.parse()中用到了 // 下划线前缀表明它是类内部使用的,而不属于类的公有API的部分 Complex._format = /^\{([^,]+), ([^}]+)\}$/;
从下例中所定义的Complex类可以看出,我们用到了构造函数、实例字段、实例方法、类字段和类方法,看一下这段示例代码:
var c = new Complex(2,3); // 使用构造函数创建新的对象 var d = new Complex(c.i,c.r); // 用到了c的实例属性 c.add(d).toString(); // => "{5,5}": 使用了实例的方法 // 这个稍微复杂的表达式用到了类方法和类字段 Complex.parse(c.toString()); // 将c转换为字符串 add(c.neg()). // 加上它的负数 equals(Complex.ZERO) // 结果应当永远是"零"
尽管JavaScript可以模拟出Java式的类成员,但Java中有很多重要的特性是无法在JavaScript类中模拟的。首先,对于Java类的实例方法来说,实例字段可以用做局部变量,而不需要使用关键字this来引用它们。JavaScript是没办法模拟这个特性的,但可以使用with语句来近似地实现这个功能(但这种做法并不推荐):
Complex.prototype.toString = function() { with(this) { return "{" + r + "," + i + }"; } };
在Java中可以使用final声明字段为常量,并且可以将字段和方法声明为private吗,用以表示它们是私有成员且在类的外面是不可见的。在JavaScript中没有这些关键字。下例中使用了一些命名写法上的约定来给出一些暗示,比如哪些成员是不能修改的(以大写字母命名的命名),哪些成员在类外部是不可见的(以下划线为前缀的命名)。私有属性可以使用闭包里的局部变量来模拟,常量属性可以在ECMAScript5中直接实现。