TDD测试驱动的javascript开发(3) ------ javascript的继承

说起面向对象,人们就会想到继承,常见的继承分为2种:接口继承和实现继承。接口继承只继承方法签名,实现继承则继承实际的方法。

由于函数没有签名,在ECMAScript中无法实现接口继承,只支持实现继承。

1. 原型链

1.1 原型链将作为实现继承的主要方法,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

构造函数---原型---实例 之间的关系:

每一个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

function SuperType() {
	this.property = true;
}

SuperType.prototype.getSuperValue = function() {
	return this.property;
};

function SubType() {
	this.subproperty = false;
}

SubType.prototype = new SuperType();    //通过原型链实现继承

SubType.prototype.getSubValue = function() {
	return this.subproperty;
};

var subInstance = new SubType();

var superInstance = new SuperType();

TestCase("test extends",{
	
	"test superInstance property should be true" : function() {
		assertEquals(true,superInstance.property);
	},"test superInstance getSuperValue() should be return true" : function() {
		assertEquals(true,superInstance.getSuperValue());
	},"test subInstance property should be false" : function() {
		assertEquals(false,subInstance.subproperty);
	},"test subInstance could visit super method " : function() {
		assertEquals(true,subInstance.getSuperValue());    //SubType继承SuperType,并调用父类的方法
	}
});

注:要区分开父类和子类的属性名称,否则子类的属性将会覆盖父类的同名属性值:看如下代码:


function SubType() {
	this.property = false;
}
SubType.prototype.getSubValue = function() {
	return this.property;
};
function SuperType() {
	this.property = true;
}

SuperType.prototype.getSuperValue = function() {
	return this.property;
};

SubType.prototype = new SuperType();    //通过原型链实现继承


var subInstance = new SubType();

var superInstance = new SuperType();

TestCase("test extends",superInstance.property);    //父类的property值为true
	},superInstance.getSuperValue());   //superInstance调用方法
	},subInstance.property);     //子类的property属值为false
	},"test subInstance could visit super method " : function() {
		assertEquals(false,并调用父类的方法,可以属性被覆盖了,返回false
	}
});
续:当然,如果我们不要求对属性值进行初始化的时候,就不必考虑这个问题,我们会采用上一章讲的构造函数模式+原型模式来创建类和实现继承关系。

当以读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性,则继续在实例的原型中寻找。在通过原型链实现继承的情况下,会继续沿着原型链继续向上

subExtends.getSuperValue()
首先在实例中查找,然后在SubType.prototype,最后在SuperType.prototype中找到。

补充: 所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的原因。

1.2 确定原型和实例的关系

方法一:使用instanceof 操作符 ---- 只要该实例是原型链中出现过的构造函数,结果就会返回true

function SuperType() {
	this.property = true;
}

SuperType.prototype.getSuperValue = function() {
	return this.property;
};

function SubType() {
	this.subproperty = false;
}

SubType.prototype = new SuperType();    //通过原型链实现继承

SubType.prototype.getSubValue = function() {
	return this.subproperty;
};

var subInstance = new SubType();

TestCase("test extends",{
	
	"test subInstance should instanceof Object" : function() {
		assertInstanceOf(Object,subInstance);
	},"test subInstance should instanceof SuperType " : function() {
		assertInstanceOf(SuperType,"test subInstance should instanceof SubType " : function() {
		assertInstanceOf(SubType,subInstance);
	}
});

方法二:使用isPrototypeOf()方法 ---- 只要原型链中出现过该原型,都可以说是该原型链所派生的实例的原型,结果会返回true

function SuperType() {
	this.property = true;
}

SuperType.prototype.getSuperValue = function() {
	return this.property;
};

function SubType() {
	this.subproperty = false;
}

SubType.prototype = new SuperType();    //通过原型链实现继承

SubType.prototype.getSubValue = function() {
	return this.subproperty;
};

var subInstance = new SubType();

TestCase("test extends",{
	
	"test subInstance isPrototypeOf  Object" : function() {
		assertEquals(true,Object.prototype.isPrototypeOf(subInstance));
	},"test subInstance isPrototypeOf SuperType " : function() {
		assertEquals(true,SuperType.prototype.isPrototypeOf(subInstance));
	},"test subInstance isPrototypeOf SubType " : function() {
		assertEquals(true,SubType.prototype.isPrototypeOf(subInstance));
	}
});

注:在实践中,我们很少会单独的使用原型链,因为它存在两个问题:

一、引用类型值的原型:包含引用类型值的原型属性会被所有实例共享,这也正是为什么要在构造函数中定义属性,而不在原型中定义属性的原因

二、创建子类型的实例时,不能向超类型的构造函数中传递参数。

1.3 借用构造函数(伪造对象 ---- 经典继承)

原理: 在子类型构造函数的内部调用超类型的构造函数

function SuperType(name) {
	this.name = name;
	this.friends = ['tong','feng'];
}

function SubType(name,age) {
	SuperType.call(this,name);
}

var subInstance = new SubType("tongtong",26);

subInstance.friends.push('ty');

var subInstance2 = new SubType("fengfeng",27);

TestCase("test constructor extends",{
	"test subInstance friends property" : function() {
		assertEquals("ty",subInstance.friends[2]);    //将ty  push到数组
	},"test subInstance2 friends length" : function() {
		assertEquals(2,subInstance2.friends.length);   //subInstance2 friends属性没有改变
	}
});

1.4 组合继承(经典伪继承) ----- 推荐模式

将原型链和借用构造函数的技术组合到一块,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承

function SuperType(name) {
	this.name = name;
	this.friends = ['tong','feng'];
}
SuperType.prototype.sayName = function() {
	return this.name;
};

function SubType(name,age) {
	//继承属性
	SuperType.call(this,name);                   //调用构造函数
	this.age = age;
}
//继承方法
SubType.prototype = new SuperType();                //调用构造函数

SubType.prototype.sayAge = function() {
	return this.age;
};

var subInstance = new SubType("tongtong",27);

TestCase("test extends",{
	
	"test subInstance name property" : function() {
		assertEquals("tongtong",subInstance.sayName());
	},"test subInstance2 name property" : function() {
		assertEquals("fengfeng",subInstance2.sayName());
	},"test subInstance friends property" : function() {
		assertEquals("ty",subInstance2.friends.length);   //subInstance2 friends属性没有改变
	}
});

这种继承的缺点:将会2次调用构造函数,性能一般,解决办法:参见1.7寄生组合式继承

1.5 原型式继承

这种方法没有严格意义上的构造函数,思想是借助原型可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型。

它要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象,可以把它传递给object()函数,然后该函数就会返回一个新对象。

function object(o) {
	function F() {
	}

	F.prototype = o;
	return new F();
}

var person = {
	name : "tongtong",friends : [ "feng","tong" ]
};

var another = object(person);
another.name = "newtong";
another.friends.push('lan');

var other = object(person);

TestCase("test prototype extends",{
	
	"test another is extends  person name property" : function() {
		assertEquals("newtong",another.name);
	},"test person firends property length is 3" : function() {
		assertEquals(3,person.friends.length);
	},"test other firends property length is 3" : function() {
		assertEquals(3,other.friends.length);
	}
});

person 作为另一个对象的基础,我们把它传入到object()中,然后该函数就会返回一个新对象,这个对象将person做为原型。

ECMAScript通过Object.create()方法规范了原型式继承,在传入一个参数的时候,Object.create()与object()方法的行为相同。

var person = {
	name : "tongtong","tong" ]
};
var another = Object.create(person);
another.name = "lisa";
another.friends.push("lan");
TestCase("test prototype extends",{
	
	"test another is extends  person name property" : function() {
		assertEquals("lisa",person.friends.length);
	}
});

如果传入2个参数,第二个参数会覆盖同名参数

var person = {
	name : "tongtong","tong" ]
};
var another = Object.create(person,{
	name : {
		value : "claire"
	}
});

another.friends.push('lalala');
TestCase("test prototype extends",{
	
	"test another is extends  person name property" : function() {
		assertEquals("claire",person.friends.length);
	}
});

在只想让一个对象与另一个对象保持类似的情况下,又没必要创建构造函数的时候,原型式继承OK.(它和原型模式一样哦,引用类型的值都会被共享)。

1.6寄生式继承

思路:与寄生构造和工厂模式类似,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像它真的做了所以工作一样返回。

//这是一个函数哦
function createAnother(original) {
//	var clone = object(original);   //创建一个新对象
	var clone = Object.create(original);
	clone.sayThis = function() {        //增强对象
		return original; 
	};
	return clone;                   //返回对象
};

var person = {
		name : "tong",friends : ['tong','feng']
};

var another = createAnother(person);

TestCase("test prototype extends",{
	"test another is extends  person name property" : function() {
		assertEquals("tong",another.sayThis().name);
	}
});
;
缺点:使用寄生式继承来为对象添加函数,会因为函数不能复用而降低效率(和构造函数模式相似)

1.7寄生组合式继承 ------ 最佳方案

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法,

思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非就是超类型原型的一个副本而已。

本质上:使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

特点:寄生式继承是引用类型最理想的继承方式

function SuperType(name) {
	this.name = name;
	this.colors = ['red','green'];
}

SuperType.prototype.sayName = function() {
	return this.name;
};
/**
 * 1.创建父类型的一个副本
 * 2.弥补创建副本时所丢失的constructor属性
 * 3.将新的副本赋值给子类型的原型
 * @param subType
 * @param superType
 */
function inheritPrototype(subType,superType) {
	var object = Object.create(superType.prototype);//创建对象
	object.constructor = subType;         //增强对象
	subType.prototype = object;			//指定对象
}

function SubType(name,name);   //调用构造函数
	this.age = age;
}

inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function() {
	return this.age;
};



var subInstance1 = new SubType('feng',28); 
subInstance1.colors.push('yellow');

var subInstance2 = new SubType('tong',25);

TestCase("test parasitic extends",{
	"test subInstance1 name property should be feng" : function() {
		assertEquals("feng",subInstance1.name);    
	},"test subInstance2 name property should be tong" : function() {
		assertEquals('tong',subInstance2.name);   
	},"test subInstance1 colors property length should be 3" : function() {
		assertEquals(3,subInstance1.colors.length);   
	},"test subInstance2 colors property length should be 2" : function() {
		assertEquals(2,subInstance2.colors.length);   
	},"test subInstance1 sayAge method should be return 28" : function() {
		assertEquals(28,subInstance1.sayAge());   
	},"test subInstance2 sayAge method should be return 25" : function() {
		assertEquals(25,subInstance2.sayAge());   
	},"test subInstance1 should be instanceof SuperType" : function() {
		assertInstanceOf(SuperType,subInstance1);   
	}
	
});


1.8总结

1.8.1 ECMAScript 支持面向对象编程,但不使用类或者接口,对象可以在代码执行过程中创建和增强。

1.8.2创建对象的几种方式:

构造函数模式:可以创建自定义的引用类型,使用new操作符

缺点:在每个实例上都要重新创建,无法复用,包括函数

优点:与对象的松耦合

适用场合:当属性或者方法不做共享属性或者方法的时候(比如引用类型的属性),不建议单独使用。

原型模式:使用构造函数的prototype属性来指定那些共享的属性和方法

优点:所有成员都可以共享属性和方法

缺点:没有私有属性值

适用场合:所以的属性和方法都可以被共享的时候,不建议单独使用

组合使用构造函数和原型模式:使用构造函数定义实例属性,使用原型定义共享属性和方法。

优点:解决了原型模式共享引用类型的属性的问题,也解决了构造函数不能共享属性的问题。

缺点:实现继承的时候,将会2次调用父类型的构造函数。性能问题。

使用场合:使用最广泛的定义引用类型的一种默认模式。

动态原型模式:保持了构造函数和原型的优点,把所有的信息封装在构造函数内,在有必要的情况下进行初始化原型。

稳妥构造函数模式:适合在安全环境中使用。

1.8.3javascript的继承:

javascript主要通过原型链事实现继承。

原型链的继承:原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。

缺点:对象实例共享所有继承的属性和方法,因此不宜单独使用。

借用构造函数继承:在子类构造函数的内部调用超类型的构造函数。call() apply()

缺点:没有函数的复用,不建议单独使用

组合继承:使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

缺点:将会调用两次构造函数,会有性能问题

原型式继承:可以在不必预定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制,而复制的副本还可以得到进一步的改造。

优点:在没有必要创建构造函数,只是让一个对象与另一个对象保持类似的情况下,原型式继承OK.

缺点:共享属性和方法

寄生式继承:与原型式继承非常相似,基于对象获某些信息创建一个对象,然后增强对象,最后返回对象

优点:解决组合继承多次调用父类的构造函数而导致低效率问题。(可以与组合模式一起使用)

缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,与构造函数模式类似。

寄生组合式继承:集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结