关于继承 (一)

什么是继承?继承的目的是做什么?

继承是面向对象中的一个重要概念。它是一种方式,一种可以使“子类”获取到“父类”的方法和属性而不需要进行重写的方式。

在Es 5中继承可以使一个对象获取到另外一个对象的属性和方法。

试想这样一些情况,你接手了一个中途的项目,前辈将函数已经封装在了某个对象中,而你需要对这个项目添加功能,这时候你又不想重写这个项目,你需要用到前面的属性和方法,这个时候你就可以使用继承来让你写的对象来访问前面对象的方法和属性。

可以说继承的目的就是复用前面的代码,减少代码的重复性。

Es5 中是如何实现继承的?

关于继承这一部分的内容总感觉有些理解的不够透彻,在网上看过很多博客,最后感觉阮一峰老师的博客讲的最为透彻。我在这里将这一部分复述一下添加一些个人的理解。

Es 5中继承方式可以分为构造函数的继承和非构造函数的继承。

构造函数的继承

构造函数绑定

第一种方式是利用call 和 apply 方法,让子对象获取父对象属性和方法。

function Animal(){
	this.colors = ["red","blue","green"];
}
function Cat(){
	Animal.call(this);
	this.name = name;
}

var zx = new Cat("zx");
alert(zx.colors);	// red,blue,green

这种结构的会让子构造函数的每个实例对象都执行父构造函数中的代码,就上面的代码而言就是为每个实例对象创建一个 colors 属性。这种方法可以看做实现对构造函数模式的继承,可以复用之前构造函数里面的代码。但它的缺点如同构造函数模式一样,如果所有对象都具有一个相同的属性或者方法,这种方法会造成内存浪费。

原型链的继承

前面学习过原型模式,在原型模式中的一个类型中,我们可以通过建立 prototype 对象来储存所有实例对象都可以访问的属性。在 Es5 中,我们可以通过重定位构造函数的 prototype 属性来指向新的原型对象,当这个原型对象等于另外一个类型的实例对象的时候,那么当前这个类型单元的原型对象就可以访问到另外一个类型的原型对象里面的属性和方法,进一步将,当前类型的实例对象就可以访问另外一个类型的原型对象的属性和方法。这样层层递进,也就组成了原型链。

这也是 Es5 中最常用的继承方式:

function Animal(){
}
Animal.prototype = {
	constructor : Animal,
	kind : 'Cat',
	showkind : function(){
		alert(this.kind);
	}
}

function Cat(name){
	this.name = name;
}

// 重定向 prototype 属性指向 Animal 的实例对象
Cat.prototype = new Animal();

// 设置新原型对象的 constructor 属性,不设置的情况下,会调用 Animal.prototype 中的 constructor 属性指向 Animal
Cat.prototype.constructor = Cat;

var zx = new Cat('zx');
zx.showkind();			// Cat

这种继承方式的缺点在于,当我们对父类型原型对象中的引用类型进行更改的时候,会对所有实例对象造成影响

function SuperTest(){
	this.colors = ["red","blue","green"]
}

SuperTest.prototype = {
	constructor : SuperTest,
	showColors : function(){
		alert(this.colors);
	}
}

function Cat (name){
	this.name = name
}

Cat.prototype = new SuperTest();
Cat.prototype.Constructor = Cat;

var zx = new Cat('zx');
var xf = new Cat('xf');

zx.colors.push('white');

xf.showColors();	// red,blue,green,white

这与学习原型中所讲的原型模式的缺点相同,同时,无法满足需要向父类型传参的情况。

组合上面的继承方法

我们可以结合构造函数绑定的方法来为每个对象建立一个私有属性来避免原型链继承的缺点。这种组合的方法也同时支持向父类型传参。

function SuperTest(kind){
	this.kind = kind
}

SuperTest.prototype = {
	constructor : SuperTest,
	showKind : function(){
		alert(this.kind);
	}
}

function Cat (kind,name){
	SuperTest.call(this,kind);
	this.name = name
}

Cat.prototype = new SuperTest();
Cat.prototype.Constructor = Cat;

var zx = new Cat('Cat','zx');

zx.showKind();	// Cat

要注意的是结合构造函数与原型链的方法,位于父类型实例对象的私有属性会分别在子对象的实例对象和父对象实例对象上新建一次。这会造成内存的浪费。为了解决这一问题,我们可以引入一个空对象作为中介。

function SuperTest(){
	this.colors = ['red','green','blue'];
}
SuperTest.prototype = {
	constructor : SuperTest,
	showColors : function(){
		alert(this.colors);
	}
}

// 定义空对象
var F = function(){
}
// 让构造函数 F 的 prototype 属性指向 SuperTest 的原型对象
F.prototype = SuperTest.prototype;
// !!!!要注意的是,这里 F 的原型对象是存在问题的
// 由于直接指向父类型原型对象,无法重写 constructor 属性 

// 调用apply 获取父类型原应有的私有属性 添加为新实例的私有属性
function Cat (name){
	SuperTest.apply(this)
	this.name = name
}
Cat.prototype = new F();
// 这里重写 constructor 属性,更改的是 F 的实例对象的属性,不会更改 F.prototype 属性
Cat.prototype.constructor = Cat;

var zx = new Cat('zx');
var xf = new Cat('zx');

zx.showColors();	// red,green,blue

zx.colors.push('white');

zx.showColors();	// red,green,blue,white
xf.showColors();	// red,green,blue
组合版本的改进方法

对上面代码进行改进整理:

function SuperTest(){
	this.colors = ['red','green','blue'];
}
SuperTest.prototype = {
	constructor : SuperTest,
	showColors : function(){
		alert(this.colors);
	}
}
// 调用apply 获取父类型原应有的私有属性 添加为新实例的私有属性
function Cat (name){
	SuperTest.apply(this)
	this.name = name
}

// 封装空对象的步骤
function extend(super,sub){
	// 定义空对象
	var F = function(){}
	// 让构造函数 F 的 prototype 属性指向 SuperTest 的原型对象
	F.prototype = super.prototype;
	// !!!!要注意的是,这里 F 的原型对象是存在问题的
	// 由于直接指向父类型原型对象,无法重写 constructor 属性 

	sub.prototype = new F();
	// 这里重写 constructor 属性,更改的是 F 的实例对象的属性,不会更改 F.prototype 属性
	sub.prototype.constructor = sub;
}


extend(SuperTest,Cat);

var zx = new Cat('zx');
var xf = new Cat('zx');

zx.showColors();	// red,green,blue

zx.colors.push('white');

zx.showColors();	// red,green,blue,white
xf.showColors();	// red,green,blue
拷贝继承

阮一峰老师在博客中还提到一种继承方式,即通过拷贝的方式,利用 in 操作符会遍历原型对象属性这一特点来拷贝复制。不过,我个人不提倡这种做法。这种做法的缺点很多:

缺点:
1. 混淆了原来父类型的私有属性和方法和原型对象中的属性和方法,可能出现例如原型链继承的第一种错误。
2. for in 遍历效率较低,当原型链较长的时候好费时间严重
3. 当属性中存在引用类型的时候,会出现按类型引用的情况,即浅复制	


function extend2(Child, Parent) {
	var p = Parent.prototype;
	var c = Child.prototype;
	for (var i in p) {
		c[i] = p[i];
	}
	c.uber = p;
}
posted @ 2017-08-12 17:44  我是一个毛毛虫  阅读(151)  评论(0)    收藏  举报