JavaScript中Java式的类继承

作者:admin     字体:[增加 减小]    类型:原创
JavaScript的继承是通过prototype原型对象来完成的,它可以在一定程度上模拟Java等强类型语言的类继承,但是JavaScript有其自己的特点,编程的时候要活学活用。

如果你有过Java或其他类似强类型面向对象语言的开发经历的话,在你的脑海中,类成员的模样可能会是这个样子:

  • 实例字段 它们是基于实例的属性或变量,用以保存独立对象的状态
  • 实例方法 它们是类的所有实例所共享的方法,由每个独立的实例调用
  • 类字段 这些属性或变量是属于类的,而不是属性类的某个实例的
  • 类方法 这些方法是属于类的,而不是属于类的某个实例的

JavaScript和Java的一个不同之处在于,JavaScript中的函数都是以值的形式出现的,方法和字段之间并没有太大的区别。如果属性是函数,那么这个属性就定义一个方法;否则,它只是一个普通的属性或“字段”。尽管存在诸多差异,我们还是可以用JavaScript模拟出Java中的这四种类成员类型。JavaScript中的类牵扯三种不同的对象,三种对象的属性的行为和下面三种类成员非常相似:

  • 构造函数对象 构造函数(对象)为JavaScript的类定义了名字。任何添加到这个构造函数对象中的属性都是类字段和类方法
  • 原型对象 原型对象的属性被类的所有实例所继承,如果原型对象的属性值是函数的话,这个函数就作为类的实例的方法来调用
  • 实例对象 类的每个实例都是一个独立的对象,直接给这个实例定义的属性是不会为所有实例对象所共享的。定义在实例上的非函数属性,实际上是实例的字段。

在JavaScript中定义类的步骤可以缩减为一个分三步的算法。第一步,先定义一个构造函数,并设置初始化新对象的实例属性。第二步,给构造函数的prototype对象定义实例的方法。第三步,给构造函数类字段和类属性。我们可以将这三个步骤封装进一个简单的defineClass()函数中。

//定义一个扩展函数,用来将第二个以及后续参数复制至第一个参数
//这里我们处理了IE bug:在多数IE版本中
//如果o的属性拥有一个不可枚举的同名属性,则for/in循环
//不会枚举对象o的可枚举属性,也就是说,将不会正确处理诸如toString的属性
//除非我们显式检测它
var extend = (function(){
    //在修复它之前,首先检查是否存在bug
    for ( var p in {toString:null}) {
        //如果代码执行到这里,那么for/in循环会正确工作并返回
        return function extend(o){
            o = o || new Object;
            for ( var i=1;i<arguments.length;i++ ){
                var source = arguments[i];
                for (var prop in source) o[prop] = source[prop];
            }
            return o;
        };  
    }
    //如果代码执行到这里,说明for/in循环不会枚举测试对象的toString属性
    //因此返回另一个版本的extend()函数,这个函数显式测试
    //Object.prototype中的不可枚举属性
    return function patched_extend( o ) {
        o = o || new Object;
        for( var i=1; i<arguments.length; i++ ){
            var source = arguments[i];
            //复制所有的可枚举属性
            for (var prop in source) o[prop] = source[prop];
            //现在检查特殊属性
            for ( var j=0;j<protoprops.length;j++ ){
                prop = protoprops[j];
                if ( source.hasOwnProperty(prop) ) o[prop] = source[prop];
            }
        }
        return o;
    };
    //这个列表列出了需要检查的特殊属性
    var protoprops = ["toString","valueOf","constructor","hasOwnProperty",
        "isPrototypeOf","propertyIsEnumerable","toLocaleString"];
}());

//一个用以定义简单类的函数
function defineClass(constructor,   //用以设置实例的属性的函数
                     methods,       //实例的方法,复制到原型中
                     statics)       //类属性,复制到构造函数中
{
    if (methods) extend(constructor.prototype,methods);
    if (statics) extend(constructor,statics);
    return constructor; 
}

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);}}                
);

var r = new SimpleRange(1,5);
console.log( r.includes(3) );

关于上例中的Range类在文章《工厂函数和构造函数在定义类时的比较》中有详细介绍。

尽管JavaScript可以模拟出Java式的类成员,但Java中有很多重要的我是无法在JavaScript类中模拟的。首先,对于Java类的实例方法来说,实例字符可以用做局部变量,而不需要使用关键字this来引用它们。JavaScript是没办法模拟这个特性的。在Java中可以使用final声明字段为常量,并且可以将字段和方法声明为private,用以表示它们是私有成员且在类的外面是不可见的。在JavaScript中没有这些关键字。但是我们可以使用一些命名写法上的约定来给出一些暗示,比如哪些成员是不能修改的(以大写字母命名),哪些成员在类外部是不可见的(以下划线为前缀的命名)。

下面是个复数类实例,代码有些长,希望大家认真阅读。

/*
这个构造函数为它所创建的每个实例定义了实例字段的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
它们只对其参数进行操作
*/
//这里定义了一些对复数运算有帮助的类字段
//它们的命名全都是大写,用以表明它们是常量
//在ECMAScript5中,还能设置这些类字段的属性为只读
Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,1);
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 = /^\{([^,]+),([^}]+)\}$/;

var c = new Complex(2,3);   //使用构造函数创建新的对象
var d = new Complex(c.i,c.r);   //用到了c的实例属性
console.log(c.add(d).toString());   //
//这个稍微复杂的表达式用到了类方法和类字段
console.log(Complex.parse(c.toString()).    //将c转换为字符串
    add(c.neg()).   //加上它的负数
    equals(Complex.ZERO));  //结果应当永远是“零”