javascript 学习笔记之面向对象编程(二):继承&多态

时间:2023-02-01 23:12:33

~~接上篇~~上一篇实现了类的实现以及类成员变量和方法的定义,下面我们来了解下面向对象中两个最重要的特性:继承和多态

继承

js中同样可以实现类的继承这一面向对象特性,继承父类中的所有成员(变量和属性),同时可扩展自己的成员,下面介绍几种js中实现继承的方式:

1. 对象模仿(冒充):通过动态改变this指针的指向,实现对父类成员的重复定义,如下:

 //对象冒充
function ClassA(paramColor) {
this.color = paramColor;
this.sayColor = function() {
alert(this.color);
};
} function ClassB(paramColor, name) {
//冒充并实现ClassA中的成员
this.newMethod = ClassA;
this.newMethod(paramColor);
//删除掉对ClassA类冒充所使用的函数对象。
delete this.newMethod; this.name = name;
this.sayName = function() {
alert(this.name);
};
} var obj = new ClassB("yellow", "apple"); console.log("实例obj是否是ClassA的对象" + (obj instanceof ClassA));
console.log("实例obj是否是ClassB的对象" + (obj instanceof ClassB));

上例中我们实现了两个类,ClassA和ClassB,在ClassB的实现过程中,定义了一个函数newMethod来引用ClassA的构造函数并执行,这样就等于执行了A的构造函数,只不过此时的this指针指向的是类ClassB,故ClassA构造函数的这个模仿执行过程其实是给ClassB定义了相同的成员,最后删除这个起桥梁性质的冒充函数,执行结果如下:

javascript 学习笔记之面向对象编程(二):继承&多态

根据执行结果我们可以看出,子类ClassB定义的对象并不同属其父类的实例,这种方式实现的继承并不是实际意义上的继承, 此外,这种方式只能模仿实现父类构造函数中定义的成员,对于父类中通过prototype定义的成员将不能继承。

2. 利用apply和call方法实现继承:同第一种方式相似,这种方式是通过apply和call方法动态改变this指针的引用实现对父类成员的重复定义,下面对ClassB改写如下:

 //call方法
function ClassBEx(paramColor, name) {
ClassA.call(this, paramColor); this.name = name;
this.sayName = function() {
alert(this.name);
}
}
//aply方法
function ClassBEEx(paramColor, name) {
//如果类A的构造函数与类B的构造函数参数顺序完全相同时可用
ClassA.apply(this, arguments); this.name = name;
this.sayName = function() {
alert(this.name);
}
}

这种方式同上一种的优缺点一样,并不是实际意义上的继承。

3. 共享prototype对象实现继承:子类通过对父类prototype对象进行共享以对父类成员的定义,从而实现继承,下面对ClassA和ClassB进行重新定义:

 //类ClassA的定义
function ClassA(paramColor) {
this.color = paramColor;
}
ClassA.prototype.sayColor = function() {
console.log("执行ClassA中的成员函数sayColor:" + this.color);
}
//类ClassB的定义
function ClassB(paramColor, name) {
this.name = name;
}
//类ClassB共享使用类ClassA的prototype对象
ClassB.prototype = ClassA.prototype;
ClassB.prototype.sayName = function() {
console.log(this.name);
}
//ClassB重写了类ClassA中的函数成员
ClassB.prototype.sayColor = function() {
console.log(this.color);
} var objA = new ClassA("yellow");
var obj = new ClassB("red","apple"); console.log("实例obj的color属性" + obj.color);
console.log("实例obj是否是ClassA的对象" + (obj instanceof ClassA));
console.log("实例obj是否是ClassB的对象" + (obj instanceof ClassB));
objA.sayColor();

上面阴影部分代码实现子类ClassB对父类ClassA的prototype对象进行共享,执行结果如下:

javascript 学习笔记之面向对象编程(二):继承&多态

结果有点点意外,可以总结为以下几点:

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实现继承:在共享原型的基础上进行了改进,通过遍历基类的原型对象来给派生类原型对象赋值,以达到继承的目的,具体如下:

 //类ClassA的定义
function ClassA(paramColor) {
this.color = paramColor;
}
ClassA.prototype.sayColor = function() {
console.log("执行ClassA中的成员函数sayColor:" + this.color);
}
//类ClassB的定义
function ClassB(paramColor, name) {
this.name = name;
}
//遍历基类的原型对象来给自己的原型赋值
for (var p in ClassA.prototype) {
ClassB.prototype[p] = ClassA.prototype[p];
}
ClassB.prototype.sayName = function() {
console.log(this.name);
}
//ClassB重写了类ClassA中的函数成员
ClassB.prototype.sayColor = function() {
console.log("执行ClassB中的成员函数sayColor:red");
} var objA = new ClassA("yellow");
var obj = new ClassB("red", "apple"); console.log("实例obj的color属性" + obj.color);
console.log("实例obj是否是ClassA的对象" + (obj instanceof ClassA));
console.log("实例obj是否是ClassB的对象" + (obj instanceof ClassB));
objA.sayColor();
obj.sayColor();

上面阴影部分的代码为遍历基类(ClassA)的prototype对象然后赋值给派生类ClassB的prototype对象,实现对基类的成员进行继承,执行结果如下:

javascript 学习笔记之面向对象编程(二):继承&多态

  由上图可见,基类和派生类的prototype是独立的,派生类继承了基类prototype定义的成员,并添加和重写了基类的成员函数sayColor,它们的执行结果互不干扰,唯一的缺憾是当前这种方式仍然不能继承基类构造函数中定义的成员,这一点可以通过在派生类的构造函数中添加一行代码实现,改写派生类ClassB的定义如下:

 //类ClassB的定义
function ClassB(paramColor, name) {
ClassA.call(this, paramColor);
this.name = name;
}

这样将基类的构造函数通过this指针附加到派生类的执行上下文中执行,实现对基类构造函数中定义的成员的继承。

为了提高代码的可读性,我们改进遍历基类prototype的实现过程:

 Function.prototype.inherit = function(superClass) {
for (var p in superClass.prototype) {
this.prototype[p] = superClass.prototype[p];
}
}

通过给Function对象添加成员方法,我们给所有的函数类型对象添加了一个静态方法,实现类的继承我们可以通过下面这句代码:

 ClassB.inherit(ClassA);

从继承的角度,上面这种方式更加容易被接受,但是有一点,通过反射(遍历)结合prototype实现继承的派生类,如果需要额外定义自己的成员,则只能通过对ptototype对象定义新的属性(ClassB.prototype.newAttr=?)来实现,而不能通过无类型方式(ClassB.prototype={}),否则会覆盖掉从基类继承下来的成员。

5. 继承的优化:主要对最后一种继承机制进行优化,定义一个Extend函数,实现对从基类继承后的对象的一个扩展,从而使得派生类添加新成员时更加高效,代码实现如下:

 /*
* 将对象p中的属性全部添加到o对象中,如果存在重复,则直接覆盖
*/
function extend(o, p) {
for (prop in p) {
o[prop] = p[prop];
}
return o;
}
/*
* 创建以o对象为原型的新的对象。
* 新的对象包含o中所有的成员
*/
function inherit(o) {
if (o == null) throw TypeError();
if (Object.create) {
return Object.create(o);
}
var t = typeof p;
if (t !== "Object" && t !== "function") throw TypeError();
function f() { }
f.prototype = o;
return new f();
}
/*
* 通过Function给每个函数对象添加一个静态方法
* constructor:派生类构造函数
* methods:派生类需要新定义的成员方法
* statics:派生类需要定义的静态变量或方法的集合
* 返回派生类构造函数
*/
Function.prototype.extend = function(constructor, methods, statics) {
return definedSubClass(this, constructor, methods, statics);
}
/*
* js类继承的核心方法
* superClass:基类的构造函数(extend的执行时this指针,执行函数对象本身)
* constructor:派生类构造函数
* methods:派生类需要新定义的成员方法
* statics:派生类需要定义的静态变量或方法的集合
* 返回派生类构造函数
*/
function definedSubClass(superClass, constructor, methods, statics) {
constructor.prototype = inherit(superClass.prototype);
constructor.prototype.constructor = constructor;
if (methods) extend(constructor.prototype, methods);
if (statics) extend(cosntructor, statics);
return constructor;
}

这些都是实现类继承模板的核心函数,主要是通过Function对象给所有的函数类型的对象添加了一个静态函数,有了上面的函数,实现上面ClassB继承ClassA,我们可以改为成:

 //类ClassA的定义
function ClassA(paramColor) {
this.color = paramColor;
}
ClassA.prototype.sayColor = function() {
console.log("执行ClassA中的成员函数sayColor:" + this.color);
} //ClassA作为基类派生出ClassB
var ClassB = ClassA.extend(function(paramColor, name) {
//构造函数(成员属性由构造函数定义)
ClassA.call(this, paramColor);
this.name = name;
}, {
//新定义或者重新定义的方法
sayName: function() {
console.log(this.name);
},
sayColor: function() {
console.log("执行ClassB中的成员函数sayColor:red");
}
},
{
//无静态成员
}); var objA = new ClassA("yellow");
var obj = new ClassB("red", "apple"); console.log("实例obj的color属性" + obj.color);
console.log("实例obj是否是ClassA的对象" + (obj instanceof ClassA));
console.log("实例obj是否是ClassB的对象" + (obj instanceof ClassB));
objA.sayColor();
obj.sayColor();

阴影部分,我们通过扩展的extend函数实现了类的继承,简单明了,执行上面的例子,结果如下:

javascript 学习笔记之面向对象编程(二):继承&多态

可以看出,优化后的方法完美的实现了js类的继承中遇到的几个问题。

多态

  面向对象编程中的多态主要是通过抽象类和抽象函数实现的,js中也可以从这两个方面实现多态。传统意义上的多态,是通过派生类继承并实现基类中的抽象(虚)函数来实现的,含有抽象函数的类是抽象类,抽象类是不能够实例化的,同时,抽象函数没有函数体,也不能够直接调用,只能有派生类继承并实现。在高级程序语言中,上述这些检测均在程序编译时进行,不符合要求的程序编译将不通过,但是在js中,有了些许变化:

1. js是解释性语言,不需要进行预编译,所以js中抽象类和抽象函数的使用并没有那么严格的要求。

2. js中可以对未定义的方法进行调用,当然这一过程会报错,而检测时在执行调用时进行的。

所以,js中的抽象类可以定义实例,但就其意义而言,我们可以定义一个空的没有成员的类来代替,同样,js中的抽象函数,我们可以不必在基类中声明,直接进行调用,在派生类中实现即可,当然,也可以通过在基类中定义一个空的抽象方法实现,代码如下:

 function ClassA() {
//抽象类,类的实现过程为空
}
ClassA.prototype = {
sayColor: function() {
//直接调用抽象方法
this.initial();
},
//定义一个空的抽象方法由派生类去实现,也可以不定义
initial: function() { }
} //ClassA作为基类派生出ClassB
var ClassB = ClassA.extend(function(name) {
this.name = name;
}, {
//实现基类中的抽象方法
initial: function() {
console.log(this.name);
}
},
{
//无静态成员
});

这样的实现与真正意义上的多态相差有点大,可能会让人疑惑这种必要性,为了最大程度的满足严格意义上的多态,我们改写上面的代码如下:

 //抽象类
function ClassA() { throw new Error("can't instantiate abstract classes."); }
ClassA.prototype = {
initial: function() { throw new Error("can't call abstract methods."); }
} //ClassA作为基类派生出ClassB
var ClassB = ClassA.extend(function(name) {
this.name = name;
}, {
//实现基类中的抽象方法
initial: function() {
console.log(this.name);
}
},
{
//无静态成员
});

为了不让抽象类实例化,我们直接在其构造函数中抛出异常,为了不能直接调用抽象方法,我们也直接在其抽象方法中抛出异常,这样我们就满足了抽象类/方法的严格要求。

  至此,JavaScript中面向对象的实现就结束了,其类的实现也是一种模块化,这样代码的可读性就一步加强,具体在我们的日常工作中,很少会可以这样封装,也没有必要,但在大型web应用中,用模块化、抽象化来重构js代码将显得比较迫切,用面向对象去面对需求的多样性,以最少的改动去满足新的需求,何乐而不为,因为分享,所以快乐,在与大家交流中成长~~~

javascript 学习笔记之面向对象编程(二):继承&多态的更多相关文章

  1. javascript 学习笔记之面向对象编程(一):类的实现

    ~~想是一回事,做是一回事,写出来又是一回事~~一直以来,从事C++更多的是VC++多一些,从面向过程到面向对象的转变,让我对OO的编程思想有些偏爱,将一个客观存在的规律抽象出来总是让人比较兴奋,通过 ...

  2. JavaScript学习笔记之数组(二)

    JavaScript学习笔记之数组(二) 1.['1','2','3'].map(parseInt) 输出什么,为什么? ['1','2','3'].map(parseInt)//[1,NaN,NaN ...

  3. Java程序猿JavaScript学习笔记(2——复制和继承财产)

    计划和完成在这个例子中,音符的以下序列: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaSc ...

  4. C++ Primer 学习笔记_67_面向对象编程 --转换与继承、复制控制与继承

    面向对象编程 --转换与继承.复制控制与继承 I.转换与继承 引言: 由于每一个派生类对象都包括一个基类部分,因此能够像使用基类对象一样在派生类对象上执行操作. 对于指针/引用,能够将派生类对象的指针 ...

  5. C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

    面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:假设不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这样的类作用域的层次嵌套使 ...

  6. C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

    面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对 ...

  7. python自动化测试学习笔记-7面向对象编程,类,继承,实例变量,邮件

    面向对象编程(OOP)术语: class TestClass(object):   val1 = 100       def __init__(self):     self.val2 = 200   ...

  8. python 学习笔记7 面向对象编程

    一.概述 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发"更快更好更强..." ...

  9. python学习笔记(七):面向对象编程、类

    一.面向对象编程 面向对象--Object Oriented Programming,简称oop,是一种程序设计思想.在说面向对象之前,先说一下什么是编程范式,编程范式你按照什么方式来去编程,去实现一 ...

随机推荐

  1. ASP - MSXML2.ServerXMLHTTP & HTTPS & 证书过期 — msxml3.dll '80072f05'

    Error: msxml3.dll  '80072f05' The date in the certificate is invalid or has expired Dim xmlhttp Set ...

  2. Android 通过xml 自定义图片

    <?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http:/ ...

  3. 【C&num;学习笔记】写文件

    using System; using System.IO; namespace ConsoleApplication { class Program { static void Main(strin ...

  4. 面试题之redis实现限制1小时内每用户Id最多只能登录5次

    面试题之redis实现限制1小时内每用户Id最多只能登录5次 /// <summary> /// redis实现限制1小时内每用户Id最多只能登录5次 /// </summary&g ...

  5. ASP&period;NET4&period;0新特性

    原文:ASP.NET4.0新特性 在以前试用VS2010的时候已经关注到它在Web开发支持上的一些变化了,为此我还专门做了一个ppt,当初是计划在4月12日那天讲的,结果因为莫名其妙的原因导致没有语音 ...

  6. rune is alias of int32

    I think chendesheng's quote gets at the root cause best: Go uses a lot of signed values, not just fo ...

  7. linux shell &dollar; 特殊变量

    $0   #Shell本身的文件名 $1-$n   #添加到Shell的各参数值.$1是第1参数.$2是第2参数… $*   #所有参数列表.如"$*"用「"」括起来的情 ...

  8. is&lowbar;readable&lpar;&rpar; 函数检查指定的文件是否可读。

    定义和用法 is_readable() 函数判断指定文件名是否可读. 语法 is_readable(file) 参数 描述 file 必需.规定要检查的文件. 说明 如果由 file 指定的文件或目录 ...

  9. 图解HTTP &sol; HTTPS

    http://kb.cnblogs.com/page/155287/ 我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取.所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议 ...

  10. &lbrack;py&rsqb;&lbrack;mx&rsqb;django使用class写views-免去判断方法的烦恼

    修改views使用class模式 类模式写views - 免去了函数模式的判断的烦恼 users/views.py from django.views.generic import View clas ...