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中没有这些关键字。但是我们可以使用一些命名写法上的约定来给出一些暗示,比如哪些成员是不能修改的(以大写字母命名),哪些成员在类外部是不可见的(以下划线为前缀的命名)。

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

```brush:js /* 这个构造函数为它所创建的每个实例定义了实例字段的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.rthat.r-this.ithat.i,this.rthat.i+this.ithat.r); }

//计算得数的模,得数的模定义为原点(0,0)到复平面的距离 Complex.prototype.mag = function() { return Math.sqrt(this.rthis.r + this.ithis.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