Javascript基础知识篇(4): 面向对象之继承
在我们经常面对的面向对象高级语言中,继承早已是老生常谈的话题了。但对于javascript这门极富灵活性的语言来说,继承却是一门复杂的技术。那么继承到底能带来什么好处呢?如果我们希望减少更多的重复性工作,弱化对象间的耦合性,在现有类的基础上并充分利用已具备的方法进行设计,那么继承不失为一种更好的解决方案了。为了能达到我们想要的目标,可以采用类式继承,也可以采用原型式继承。可能有的人会问:我有两个类并没有所谓的"is a"的关系,但却都有个相同的方法要调用,我该怎么办?我们知道,在面向对象中可以采用接口去实现。在本篇文章中,我采用混元类的方式来解决这种问题并会探讨适合场景。
首先,我们从类式继承谈起。大家如果看过前面的文章的话,对在JS中如何创建一个类应该早已胸有成竹了,那我直接从例子讲解。我们定义一个简单的类Person并实例化一个对象。
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
var reader = new Person("Miracle");
reader.getName();//Miracle
接下来我们再添加一个Person的子类Author,看看如何去继承父类的方法呢?
function Author(name, books) {
Person.call(this, name);
this.books = books;
}
Author.prototype = new Person();
Author.prototype.constructor = Author;
Author.prototype.getBooks = function() {
return this.books;
}
var author = new Author("Miracle He", ["JavaScript Learning"]);
author.getName();
author.getBooks();
可能有的朋友不是很明白上面的代码了,我重点对其中几句一一解释。首先创建了Author的构造函数,我们都知道在使用new运算符创建一个对象时,首先将创建一个空对象并调用构造函数,同时这个空对象将位于作用域链的最前端,在这里Person调用自身构造函数并将name参数传入实现赋值。那接下来就是Author.prototype = new Person();大家都知道在JS中不管是什么对象都会有prototype这个属性,当在调用该对象的方法时,会首先从该对象的类寻找,如果不存在再继续到该类prototype所指的类寻找,会一直沿原型链往上寻找直到找到该方法为止。那既然我已经给了Person作为Author的原型了,那接下来将constructor属性重设为Author。从上述代码可以看出,尽管实现继承有点难度,但是在子类的调用上却非常简单。那接下来我们就重构一下我们的代码。添加了一个扩展方法:
function extend(subclass, superclass) {
var F = function() {};
F.prototype = superclass.prototype;
subclass.prototype = new F();
subclass.prototype.constructor = subclass;
}
function Author(name, books) {
Person.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
return this.books;
}
可能大家还发现,在Author的构造函数中还有Person这个固化的身影,我们赶紧把它通用化。
function extend(subclass, superclass) {
var F = function() {};
F.prototype = superclass.prototype;
subclass.prototype = new F();
subclass.prototype.constructor = subclass;
subclass.parent = superclass.prototype;
if(superclass.prototype.constructor == Object.prototype.constructor) {
superclass.prototype.constructor = superclass;
}
}
function Author(name, books) {
Author.parent.constructor.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
return this.books;
}
接下来,我开始介绍原型式继承。与类式继承完全不一样,并不需要类来定义对象的结构,而是直接创建一个对象即可。利用原型链查找的机制来为新的对象重用,即是为其他对象提供了一个模型,我们姑且称它为原型对象。下面我就利用原型式继承来重写Person和Author:
var Person= {
name: "default name",
getName: function() {
return this.name;
}
};
function clone(object) {
function F() {}
F.prototype = object;
return new F;
}
var reader = clone(Person);
alert(reader.getName());//"default name"
reader.name = "Miracle";
alert(reader.getName());//"Miracle"
可能大家最关心莫非是"clone"函数了,简单几句话就完成了一个对象的复制,最关键就是prototype起作用了。顺便提一句吧,prototype属性是用来指向原型对象的,通过原型链接机制提供给所有继承而来成员的链接。那要创建一个继承对象Author就简单的多了。
var Author = clone(Person);
Author.books = [];
Author.getBooks = function() {
return this.books;
};
var author = clone(Author);
author.name = "Miracle He";
author.books = ["Javascript Learning"];
alert(author.getName());//"Miracle He"
alert(auhtor.getBooks());//"Javascript Learning"
我们可以看出,你可以重定义原型对象的方法和属性,同时也能定义新的方法和属性。在类式继承中,Author生成的每一个实例都有books属性的副本,但对于原型式继承生成的实例books却不总是一份完全独立的副本,而只是指向原型对象的一个空对象而已。可能大家对这句话感觉很晦涩,那我举例来说明:
var miracle = clone(Author);
var mike = clone(Author);
miracle.books.push("Javascript Learning");
alert(mike.books);//??
看了这段代码,有结果了吗?我想告诉你,mike.books也包含"Javascript Learning"这本书了,为什么呢?因为miracle.books在执行push之前,是指向Author.books(原型对象)的一个空对象,当执行push之后原型对象自然而然就拥有了miracle所push的书,而mike又是Author的继承对象,当然也就拥有了。这可能不是我们想达到的继承效果,回想平时经常说的:必须传递引用的数据类型来创建新副本。那该如何解决上述问题呢?我们可以在继承对象push之前,将books给重新清空再添加。我们再来看一个有子对象的原型对象是如何弱化对象之间的耦合的:
var GameTeams = {
name: "Dreams",
member: {
color: "blue",
count: 5
}
};
var miracle = clone(GameTeams);
miracle.member.count = 6;//不推荐(改变GameTeams.member.count)
miracle.member = {
color: "blue",
count: 6
};
//更好的办法采用工厂完成
var GameTeams = {};
GameTeams.name = "Dream";
GameTeams.createMember = function() {
return {
color: "blue",
count: 5
};
};
GameTeams.member = GameTeams.createMember();
var miracle = clone(GameTeams);
miracle.member = GameTeams.createMember();
miracle.member.count = 6;
谈完了类式继承与原型式继承,该谈谈如何混元类是什么?简单理解就是把一个函数用于多个类,使这些类以扩充的方式来共享此函数。具体做法是:先创建一个包含通用方法的类,本身不会被实例化或直接调用,而是为其他类提供自己的方法,我们叫这个类为混元类。下面以实际代码来说明:
var Mixin = function() {};
Mixin.prototype = {
serialize: function() {
var output = [];
for(key in this) {
output.push(key + ":" + this[key]);
}
return output.join(",");
}
};
以上我们就定义了一个混元类Mixin,包含一个通用方法serialize,用于遍历当前对象的所有成员并以字符串形式输出。我们可以用它来扩充Author类。
function augment(receivingClass, givingClass) {
//只扩充指定的通用方法
if(arguments[2]) {
for(var i = 2, len = arguments.length; i < len; i++) {
var method = arguments[i];
if(!receivingClass.prototype[method]) {
receivingClass.prototype[method] = givingClass.prototype[method];
}
}
} else {
for(method in givingClass.prototype) {
if(!receivingClass.prototype[method]) {
receivingClass.prototype[method] = givingClass.prototype[method];
}
}
}
}
augment(Author, Mixin);
augment(Author, Mixin, "serialize");
var author = new Author("Miracle", ["Javascript Learning"]);
var result = author.serialize();
用一些通用方法扩充多个类有时比这个类继承更适合,特别是对于彼此差异很大的类但又需要共享通用方法的情况。通过了解类式继承、原型式继承和混元类,我们来综合一下三者,写出一个通用点的实现方式,我直接给出源码,注释包含在其中。
/*给函数原型增加一个extend函数,实现继承*/
Function.prototype.extend = function(superClass) {
if(superClass === 'function') {
//类式继承
var F = function() {};//创建一个中间函数对象以获取父类的原型对象
F.prototype = superClass.prototype;//设置原型对象
this.prototype = new F();//实例化F,继承原型对象的属性和方法而无需调用父类的构造函数实例化无关的父类成员
this.prototype.constructor = this;//设置构造函数指向自己
this.superClass = superClass;//同时,添加一个指向父类构造函数的引用,方便调用父类方法或者调用父类构造函数
} else if(superClass === 'object') {
//扩充方法
var pro = this.prototype;
for(var k in superClass) {
if(!pro[k]) {
//如果原型对象不存在这个属性,则复制
pro[k] = superClass[k];
}
}
} else {
throw new Error('fatal error: "Function.prototype.extend" expects a function or a object');
}
return this;
};
用法如下:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
};
function Author(name, books) {
Author.superClass.call(this, name);
this.books = books;
}
//链式调用
Author.extend(Person).prototype.getBooks = function() {
return this.books;
};
//方法的覆写,通过superClass调用父类的方法获得基本信息,再调用子类的方法获得更特殊的信息
Author.prototype.getName = function() {
var name = Author.superClass.prototype.getName.call(this);
return name + ", Author of " + this.getBooks();
}
//扩展方法
Author.extend({
serialize: function() {
var output = [];
for(key in this) {
output.push(key + ":" + this[key]);
}
return output.join(",");
}
});
到此,关于继承的话题就谈完了,不过呢还有更深层次的东西需要了解和学习。