~~接上篇~~上一篇实现了类的实现以及类成员变量和方法的定义,下面咱们来了解下面向对象中两个最重要的特性:继承和多态。web
继承编程
js中一样能够实现类的继承这一面向对象特性,继承父类中的全部成员(变量和属性),同时可扩展本身的成员,下面介绍几种js中实现继承的方式:app
1. 对象模仿(冒充):经过动态改变this指针的指向,实现对父类成员的重复定义,以下: 模块化
1 //对象冒充 2 function ClassA(paramColor) { 3 this.color = paramColor; 4 this.sayColor = function() { 5 alert(this.color); 6 }; 7 } 8 9 function ClassB(paramColor, name) { 10 //冒充并实现ClassA中的成员 11 this.newMethod = ClassA; 12 this.newMethod(paramColor); 13 //删除掉对ClassA类冒充所使用的函数对象。 14 delete this.newMethod; 15 16 this.name = name; 17 this.sayName = function() { 18 alert(this.name); 19 }; 20 } 21 22 var obj = new ClassB("yellow", "apple"); 23 24 console.log("实例obj是不是ClassA的对象" + (obj instanceof ClassA)); 25 console.log("实例obj是不是ClassB的对象" + (obj instanceof ClassB));
上例中咱们实现了两个类,ClassA和ClassB,在ClassB的实现过程当中,定义了一个函数newMethod来引用ClassA的构造函数并执行,这样就等于执行了A的构造函数,只不过此时的this指针指向的是类ClassB,故ClassA构造函数的这个模仿执行过程实际上是给ClassB定义了相同的成员,最后删除这个起桥梁性质的冒充函数,执行结果以下:函数
根据执行结果咱们能够看出,子类ClassB定义的对象并不一样属其父类的实例,这种方式实现的继承并非实际意义上的继承, 此外,这种方式只能模仿实现父类构造函数中定义的成员,对于父类中经过prototype定义的成员将不能继承。优化
2. 利用apply和call方法实现继承:同第一种方式类似,这种方式是经过apply和call方法动态改变this指针的引用实现对父类成员的重复定义,下面对ClassB改写以下:this
1 //call方法 2 function ClassBEx(paramColor, name) { 3 ClassA.call(this, paramColor); 4 5 this.name = name; 6 this.sayName = function() { 7 alert(this.name); 8 } 9 } 10 //aply方法 11 function ClassBEEx(paramColor, name) { 12 //若是类A的构造函数与类B的构造函数参数顺序彻底相同时可用 13 ClassA.apply(this, arguments); 14 15 this.name = name; 16 this.sayName = function() { 17 alert(this.name); 18 } 19 }
这种方式同上一种的优缺点同样,并非实际意义上的继承。spa
3. 共享prototype对象实现继承:子类经过对父类prototype对象进行共享以对父类成员的定义,从而实现继承,下面对ClassA和ClassB进行从新定义:prototype
1 //类ClassA的定义 2 function ClassA(paramColor) { 3 this.color = paramColor; 4 } 5 ClassA.prototype.sayColor = function() { 6 console.log("执行ClassA中的成员函数sayColor:" + this.color); 7 } 8 //类ClassB的定义 9 function ClassB(paramColor, name) { 10 this.name = name; 11 } 12 //类ClassB共享使用类ClassA的prototype对象 13 ClassB.prototype = ClassA.prototype; 14 ClassB.prototype.sayName = function() { 15 console.log(this.name); 16 } 17 //ClassB重写了类ClassA中的函数成员 18 ClassB.prototype.sayColor = function() { 19 console.log(this.color); 20 } 21 22 var objA = new ClassA("yellow"); 23 var obj = new ClassB("red","apple"); 24 25 console.log("实例obj的color属性" + obj.color); 26 console.log("实例obj是不是ClassA的对象" + (obj instanceof ClassA)); 27 console.log("实例obj是不是ClassB的对象" + (obj instanceof ClassB)); 28 objA.sayColor();
上面阴影部分代码实现子类ClassB对父类ClassA的prototype对象进行共享,执行结果以下:指针
结果有点点意外,能够总结为如下几点:
1. 共享prototype对象能够实现子类的实例同属于父类的实例,这点可经过 instance of 返回为true看出;
2. 这种方式的继承只能继承父类prototype中的定义的父类成员,对于父类构造函数中的成员则不能继承,如上图:子类实例obj的color属性为undefined。
3. 共享原型(prototype)法,其实是使父类和子类的都引用同一个prototype对象,js中除了基本数据类型(数值、字符串、布尔类等),全部的赋值都是引用传递,而不是值传递,上述的共享致使ClassA和ClassB的prototype对象始终保持一致,因此当子类ClassB重复定义了父类中的sayColor函数后,父类中的sayColor也一样更新了,故调用父类sayColor后输出的是“red”。
4. 共享原型方法会致使基类和派生类定义本身的成员时互相干扰。
总之,此方法仍是不能实现实际意义上的继承。
4. 经过反射机制和prototype实现继承:在共享原型的基础上进行了改进,经过遍历基类的原型对象来给派生类原型对象赋值,以达到继承的目的,具体以下:
1 //类ClassA的定义 2 function ClassA(paramColor) { 3 this.color = paramColor; 4 } 5 ClassA.prototype.sayColor = function() { 6 console.log("执行ClassA中的成员函数sayColor:" + this.color); 7 } 8 //类ClassB的定义 9 function ClassB(paramColor, name) { 10 this.name = name; 11 } 12 //遍历基类的原型对象来给本身的原型赋值 13 for (var p in ClassA.prototype) { 14 ClassB.prototype[p] = ClassA.prototype[p]; 15 } 16 ClassB.prototype.sayName = function() { 17 console.log(this.name); 18 } 19 //ClassB重写了类ClassA中的函数成员 20 ClassB.prototype.sayColor = function() { 21 console.log("执行ClassB中的成员函数sayColor:red"); 22 } 23 24 var objA = new ClassA("yellow"); 25 var obj = new ClassB("red", "apple"); 26 27 console.log("实例obj的color属性" + obj.color); 28 console.log("实例obj是不是ClassA的对象" + (obj instanceof ClassA)); 29 console.log("实例obj是不是ClassB的对象" + (obj instanceof ClassB)); 30 objA.sayColor(); 31 obj.sayColor();
上面阴影部分的代码为遍历基类(ClassA)的prototype对象而后赋值给派生类ClassB的prototype对象,实现对基类的成员进行继承,执行结果以下:
由上图可见,基类和派生类的prototype是独立的,派生类继承了基类prototype定义的成员,并添加和重写了基类的成员函数sayColor,它们的执行结果互不干扰,惟一的缺憾是当前这种方式仍然不能继承基类构造函数中定义的成员,这一点能够经过在派生类的构造函数中添加一行代码实现,改写派生类ClassB的定义以下:
1 //类ClassB的定义 2 function ClassB(paramColor, name) { 3 ClassA.call(this, paramColor); 4 this.name = name; 5 }
这样将基类的构造函数经过this指针附加到派生类的执行上下文中执行,实现对基类构造函数中定义的成员的继承。
为了提升代码的可读性,咱们改进遍历基类prototype的实现过程:
1 Function.prototype.inherit = function(superClass) { 2 for (var p in superClass.prototype) { 3 this.prototype[p] = superClass.prototype[p]; 4 } 5 }
经过给Function对象添加成员方法,咱们给全部的函数类型对象添加了一个静态方法,实现类的继承咱们能够经过下面这句代码:
1 ClassB.inherit(ClassA);
从继承的角度,上面这种方式更加容易被接受,可是有一点,经过反射(遍历)结合prototype实现继承的派生类,若是须要额外定义本身的成员,则只能经过对ptototype对象定义新的属性(ClassB.prototype.newAttr=?)来实现,而不能经过无类型方式(ClassB.prototype={}),不然会覆盖掉从基类继承下来的成员。
5. 继承的优化:主要对最后一种继承机制进行优化,定义一个Extend函数,实现对从基类继承后的对象的一个扩展,从而使得派生类添加新成员时更加高效,代码实现以下:
1 /* 2 * 将对象p中的属性所有添加到o对象中,若是存在重复,则直接覆盖 3 */ 4 function extend(o, p) { 5 for (prop in p) { 6 o[prop] = p[prop]; 7 } 8 return o; 9 } 10 /* 11 * 建立以o对象为原型的新的对象。 12 * 新的对象包含o中全部的成员 13 */ 14 function inherit(o) { 15 if (o == null) throw TypeError(); 16 if (Object.create) { 17 return Object.create(o); 18 } 19 var t = typeof p; 20 if (t !== "Object" && t !== "function") throw TypeError(); 21 function f() { } 22 f.prototype = o; 23 return new f(); 24 } 25 /* 26 * 经过Function给每一个函数对象添加一个静态方法 27 * constructor:派生类构造函数 28 * methods:派生类须要新定义的成员方法 29 * statics:派生类须要定义的静态变量或方法的集合 30 * 返回派生类构造函数 31 */ 32 Function.prototype.extend = function(constructor, methods, statics) { 33 return definedSubClass(this, constructor, methods, statics); 34 } 35 /* 36 * js类继承的核心方法 37 * superClass:基类的构造函数(extend的执行时this指针,执行函数对象自己) 38 * constructor:派生类构造函数 39 * methods:派生类须要新定义的成员方法 40 * statics:派生类须要定义的静态变量或方法的集合 41 * 返回派生类构造函数 42 */ 43 function definedSubClass(superClass, constructor, methods, statics) { 44 constructor.prototype = inherit(superClass.prototype); 45 constructor.prototype.constructor = constructor; 46 if (methods) extend(constructor.prototype, methods); 47 if (statics) extend(cosntructor, statics); 48 return constructor; 49 }
这些都是实现类继承模板的核心函数,主要是经过Function对象给全部的函数类型的对象添加了一个静态函数,有了上面的函数,实现上面ClassB继承ClassA,咱们能够改成成:
1 //类ClassA的定义 2 function ClassA(paramColor) { 3 this.color = paramColor; 4 } 5 ClassA.prototype.sayColor = function() { 6 console.log("执行ClassA中的成员函数sayColor:" + this.color); 7 } 8 9 //ClassA做为基类派生出ClassB 10 var ClassB = ClassA.extend(function(paramColor, name) { 11 //构造函数(成员属性由构造函数定义) 12 ClassA.call(this, paramColor); 13 this.name = name; 14 }, { 15 //新定义或者从新定义的方法 16 sayName: function() { 17 console.log(this.name); 18 }, 19 sayColor: function() { 20 console.log("执行ClassB中的成员函数sayColor:red"); 21 } 22 }, 23 { 24 //无静态成员 25 }); 26 27 var objA = new ClassA("yellow"); 28 var obj = new ClassB("red", "apple"); 29 30 console.log("实例obj的color属性" + obj.color); 31 console.log("实例obj是不是ClassA的对象" + (obj instanceof ClassA)); 32 console.log("实例obj是不是ClassB的对象" + (obj instanceof ClassB)); 33 objA.sayColor(); 34 obj.sayColor();
阴影部分,咱们经过扩展的extend函数实现了类的继承,简单明了,执行上面的例子,结果以下:
能够看出,优化后的方法完美的实现了js类的继承中遇到的几个问题。
多态
面向对象编程中的多态主要是经过抽象类和抽象函数实现的,js中也能够从这两个方面实现多态。传统意义上的多态,是经过派生类继承并实现基类中的抽象(虚)函数来实现的,含有抽象函数的类是抽象类,抽象类是不可以实例化的,同时,抽象函数没有函数体,也不可以直接调用,只能有派生类继承并实现。在高级程序语言中,上述这些检测均在程序编译时进行,不符合要求的程序编译将不经过,可是在js中,有了些许变化:
1. js是解释性语言,不须要进行预编译,因此js中抽象类和抽象函数的使用并无那么严格的要求。
2. js中能够对未定义的方法进行调用,固然这一过程会报错,而检测时在执行调用时进行的。
因此,js中的抽象类能够定义实例,但就其意义而言,咱们能够定义一个空的没有成员的类来代替,一样,js中的抽象函数,咱们能够没必要在基类中声明,直接进行调用,在派生类中实现便可,固然,也能够经过在基类中定义一个空的抽象方法实现,代码以下:
1 function ClassA() { 2 //抽象类,类的实现过程为空 3 } 4 ClassA.prototype = { 5 sayColor: function() { 6 //直接调用抽象方法 7 this.initial(); 8 }, 9 //定义一个空的抽象方法由派生类去实现,也能够不定义 10 initial: function() { } 11 } 12 13 //ClassA做为基类派生出ClassB 14 var ClassB = ClassA.extend(function(name) { 15 this.name = name; 16 }, { 17 //实现基类中的抽象方法 18 initial: function() { 19 console.log(this.name); 20 } 21 }, 22 { 23 //无静态成员 24 });
这样的实现与真正意义上的多态相差有点大,可能会让人疑惑这种必要性,为了最大程度的知足严格意义上的多态,咱们改写上面的代码以下:
1 //抽象类 2 function ClassA() { throw new Error("can't instantiate abstract classes."); } 3 ClassA.prototype = { 4 initial: function() { throw new Error("can't call abstract methods."); } 5 } 6 7 //ClassA做为基类派生出ClassB 8 var ClassB = ClassA.extend(function(name) { 9 this.name = name; 10 }, { 11 //实现基类中的抽象方法 12 initial: function() { 13 console.log(this.name); 14 } 15 }, 16 { 17 //无静态成员 18 });
为了避免让抽象类实例化,咱们直接在其构造函数中抛出异常,为了避免能直接调用抽象方法,咱们也直接在其抽象方法中抛出异常,这样咱们就知足了抽象类/方法的严格要求。
至此,JavaScript中面向对象的实现就结束了,其类的实现也是一种模块化,这样代码的可读性就一步增强,具体在咱们的平常工做中,不多会能够这样封装,也没有必要,但在大型web应用中,用模块化、抽象化来重构js代码将显得比较迫切,用面向对象去面对需求的多样性,以最少的改动去知足新的需求,何乐而不为,由于分享,因此快乐,在与你们交流中成长~~~