OOP的特性之一就是继承,只有实现了继承的语言才能称之为OOP,本篇将说明javasctipt如何使用继承。
原型、构造函数和对象中的基于原型的继承概念
虽然本地对象都继承自Object,但实际上,可以认为所有对象都是继承自原型。在JAVA中,定义了一个根对象Object,所有对象都从根对象继承而来,从而建立基于类的对象体系。那么我们也可以认为javascript中,定义了原型对象,所有类基于原型而建立了对象体系。
这个继承过程是这样的:
1、当定义函数(构造函数也是函数)的时候,就自动为其设置了一个原型属性,原型属性的值是原型对象,原型对象只有一个函数constructor,并指向关联的构造函数。
2、以构造函数的方式定义类,类的原型的值就是这个构造函数原型的引用。
3、从类实例化对象,执行操作符new的时候,创建了一个没有属性空对象,赋值给this关键字。自动初始化对象的原型,值为构造函数的原型对象引用。
这就是原型的继承过程,在定义类的时候,给原型定义的属性和方法,就能够被有效继承,从而完整是实现了OOP的对象继承机制。
问题:在构造函数而不是构造函数的原型中定义属性和方法的方式,是否也会被类的实例对象继承呢?这个问题放到后面能够理解的时候来解答。
基于原型继承的属性读取是写入是不对称的,过程式这样的:
1、属性读取:在实例对象o中读取属性p:o.p,先从o中查找属性p,如果没有找到,从o.prototype中查找,这样基于原型的继承能够奏效;
2、属性写入:设置实例对象o属性p的值:o.p=1,则不会使用原型对象,而直接给实例对象属性p写入值,如果没有找到属性p,新增属性p并赋值,而不会深入到原型中。这样的目的是避免一个实例对象对原型的影响被扩散到其他实例对象。
Object.hasOwnProperty(/*propertyName*/)方法可以区分基于原型继承属性和常规属性。
类和实例属性和方法
类和实例的属性和方法分别称为:实例属性、实例方法、类属性、类方法,顾名思义,属性和方法是定义在类还是类实例中,并分别在类和类实例中调用,通过一个例子进行说明会更清晰易懂。javascript中可以模拟实现JAVA中的类和实例的方法、属性。
1 //类Circle 2 3 function Circle(radius){ 4 this.r=radius; //实例属性,在构造函数中定义并初始化 5 } 6 7 Circle.PI = 3.14159; //类属性,实际是构造函数的属性 8 9 Circle.prototype.area = function(){return Circle.PI*this.r*this.r;} //计算圆的面积的实例方法 10 11 Circle.max = function(a,b){ 12 if (a.r>b.r) return a; 13 else return b; 14 } //类方法定义,返回面积较大的圆对象 15 16 var c = new Circle(1.0); //创建类实例 17 c.r = 2.2; //设置r实例属性 18 19 var a=c.area(); //调用area()实例方法 20 var x=Math.exp(Circle.PI); //使用类属性 21 22 var d=new Circle(11.2) //创建另外一个实例 23 var bigger = Circle.max(c,d); //调用类方法
本质上,类属性和类方法是全局可访问的。在JAVA中,类如果没有被实例化,只有静态属性和方法能够使用,javascript中因为是全局的,可以模拟这个情况。
1、在构造函数外不添加到原型对象的属性和方法,模拟为类方法和属性,通过类直接调用。类似JAVA中的静态方法;
2、在构造函数和构造函数外添加到原型的属性和方法,模拟为实例属性和方法,需要实例化对象中调用。类似JAVA中的普通方法。
虽然在语法上没有严格限制这么做, 但这么做的好处是显而易见的。javacript对象编程具有太多的可能性,我们需要建立自己的编程风格。如果希望在类实例中使用的属性和方法,在构造函数和原型中定义,否则在构造函数外,不使用原型来定义。
类层次的继承
在JAVA等基于类继承的语言中,提供了一个根类Object,所有类都是根类的子类,类之间可以相互继承,从而建立类层次结构,javascript也采用类似类层次。Object类是通用类,是所有本地对象的超类,所有对象都继承了它部分属性。
问题:前面可能说到javascript没有基于根类来创建类层次的方式,是源于前面的理解,且找了很多资料也没有所有类都继承自Object的说话。犀牛书“超类和子类”小节明确说是基于根类Object,这个说法的根本在于类如何从Object继承。我们先耐着性子看下去。
我们已经知道了对象如何从原型继承属性,但又如何从Object继承属性呢。原型本身是一个对象,是由Object构造函数创建的,因此原型对象继承了Object.prototype属性。基于原型的继承不限于一个单一的原型对象,它包含了一个原型链,继承了原型链中所有原型的属性。比如Circle对象继承了Circle.prototype和Object.prototype的同名属性。
问题:当从函数创建类,如何从Object继承?这个过程是不是默认自动进行的?
创建javascript类的子类
遗憾的是,这不是一个清晰简单的过程,因此而备受责备。下面用一个例子来说明。
//创建一个矩形类,使用一个构造函数 function Rectangle(w,h){ this.width=w; this.height=h; } Rectangle.prototype.area=function(){ return this.width*this.height; } //至此矩形类创建完毕,下面为其创建一个子类,子类扩展父类,增加一个位置方法。 function PositionedRectangle(x,y,w,h){ //首先在新对象调用父类的构造函数,初始化宽度和高度。 //使用call方法调用构造函数 Rectangle.call(this,w,h); //现在保存矩形的位置 this.x=x; this.y=y; } /** *如果使用定义PositionedRectangle()构造函数是默认的原型对象, *将获得一个Object对象子类。为了获得Rectangle对象子类,必须显 *式创建我们自己的原型对象,原型属性的值是Rectangle对象的实例,从而PositionedRectangle具有和Rectangle对象一致的原型对象。 */ PostionedRectangle.prototype = new Rectangle(); /** * 但不需要width和height属性,需要删除 */ delete PositionedRectangle.prototype.width; delete PostiionedRectangle.prototype.height; /** *设置PositionedRectangle的原型对象的constructor属性的值是对象自身. */ PositionedRectangle.prototype.constructor=PositionedRectangle; /** *为子类配置原型对象,且为其添加方法。 */ PositionedRectangle.prototype.contains=function(x,y){ return (x>this.x && x<this.x +this.width && y>this.y && <this.y+ this.height); }
是不是想骂娘,说实话,我也很想!只是为了创建一个子类而已,且如此的复杂,且完全是技巧性的。原因在于javascript中有类对象和原型对象两种对象并存,带来非常多的复杂性,必须深刻理解原型对象继承机制,才
可能有效实现类继承。不过好在一些能够编译成javascript语言的语言,能够帮助我们,如coffeescript。
现在可以使用它。
var r=PostionedRectangle(2,2,2,2); print(r.contans(3,3)); print(r.area()); print(r.x + ", "+r.y+", "+r.width+", "+r.height); print(r instanceof PostionedRectangle && r instanceof Rectangle && r instanceof Object);
用混入方式实现非继承的扩展
说实话,第一遍看到实现类继承的时候,几乎想放弃,但javascript是目前唯一的可用于浏览器看法的语言,只能强忍“恶心”继续看下去。利用javascript的灵活性,可采取组合方式实现类似继承的效果。基于这样一种原理:javascript函数式数据值(对象也是),可以只是从一个类复制(或“借用”)函数用于另一个类。下面看例子:
/** *从一个类Borrow方法到另外一个类 *该类的构造函数必须有参数,内置类型如Object、Array、Date和RegExp不能枚举,不能用这个方法借用。 */ function borrowMethods(borrowFrom,addTo){ var from=borrowFrom.prototype; //被借的原型对象 var to =addTo.prototype; //将被扩展的原型对象 for(m in from){ //循环原型的所有属性 if(typeof from[m]!="function") continue; //忽略非函数 to[m]=from[m]; //借用该方法 } }
混入类(mixin class)
定义其他类可以借用的有用方法,除此外什么都不做,这种类叫混入类。
下面ColoredRectangle类,从Rectangle类继承矩形的功能更,并且从一个名为Colored的混入类借用了一个方法。
//混入类Colored function Colored(c){this.color=c;} Colored.prototype.getColor=function(){return this.color;} function ColoredRectangle(x,y,w,h,c){ this.superclass(x,y,w,h); //调用父类构造函数 Colored.call(this,c); //借用Colored构造函数 } //实现继承 ColoredRectangle.prototype=new Rectangle(); ColoredRectangle.prototype.constructor=ColoredRectangle; ColoredRectangle.prototype.superclass=Rectgangle; //借用方法 borrowMethods(Colored,ColoredRectangle);
这里使用了superclass方式继承父类,前面没有进行说明,是因为这个方式只能使用一次,不能用于多层次继承。
判断对象类型
typeof运算符主要用于从对象类型中区分出基本类型,一旦确定是对象而不是基本类型或者函数,就应使用instanceof运算符来判断是哪种对象类型:object、undefined、array、Function、Date等等。
鸭子类型识别(Duck Typing)
在javascript,如果它实现了一个类所定义的所有方法,它就是这个类的一个实例,即便它不是通过这个类的构造函数创建的。源于俗语:如果它走路像鸭子,叫起来像鸭子,那么它一定是鸭子!识别的方式是逐一比较类的所有属性(方法也是属性)。
工具方法:defineClass()
最后犀牛书提供了一个非常实用的定义类的方法,可以拿来使用,但基于学习目的,不建议在还没有完全掌握js的情况下使用。