【类和模块】JavaScript中Java式的类继承

【类和模块】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中直接实现。

上一篇:结对编程


下一篇:python基础-03