js-面向对象基础
声称对象的三种方法:
1.函数构造法
function funcName(){};
2.对象生成法
var obj={};
obj.x=1;
obj.y=2;
3对象直接生成法:
var obj={x:1,y:2}
属性有:
1.私有属性,公有属性,类属性与原型属性
在js中,对象的属性和方法支持4种不同的类型,第一种类型就是私有类型,它的特点就是对外界完全不具备访问性,要访问他们,只有通过特定的getter和setter。
第二种类型是动态的公有类型,它的特点是外界可以访问,而且每个对象实例持有一个副本,他们之间不会互相影响,第三种类型是静态的共有类型,通常叫做原型属性,
它的特点是每个对象实例共享唯一副本,对它的改写会相互影响。第四种类型是类属性,它的特点是作为类型的属性而不是对象实例的属性,在没有构造对象时也能够访问。
私有公有属性:
function List(){
//私有成员
var _elements=[];
//创建数组对象,同时把传入的参数加入到_elements中
_elements=Array.apply(_elements,arguments);
//这个函数中valueOfy与toString是用来模拟getLength的
//公有属性
this.length={
valueOf:function(){
return _elements.length;
},
toString:function(){
return _elements.length;
}
}
this.toString=function(){
return _elements.toString();
}
this.add=function(){
_elements.push.apply(_elements,arguments)
}
}
3getter的设置!
错误的方式:
function Point(x,y){
if(x) this.x=x;
if(y) this.y=y;
}
Point.prototype.x=0;
Point.prototype.y=0;
function LineSegment(p1,p2){
var m_firstPoint=p1;
var m_lastPoint=p2;
var m_width={
valueOf:function(){return Math.abs(p1.x-p2.x)},
toString:function(){return Math.abs(p1.x-p2.x)}
}
var m_height={
valueOf:function(){return Math.abs(p1.y-p2.y)},
toString:function(){return Math.abs(p1.y-p1.y)}
}
this.getFirstPoint=function(){
return m_firstPoint;
}
this.getLastPoint=function(){
return m_lastPoint;
}
this.length={
valueOf:function(){return Math.sqrt(m_width*m_widht+m_height*m_height)},
toString:function(){return Math.sqrt(m_width*m_width+m_height*m_height)}
}
}
var p1=new Point();
var p2=new Point(2,3)
var line1=new LineSegment(p1,p2)
var lp=line1.getFirstPoint();
//改变了lp中得x属性,因此line1中得getFirstPoint的x属性也改变了。
lp.x=100;
alert(line1.getFirstPoint().x);
alert(line1.length);
正确的方式:
function Point(x,y){
if(x) this.x=x;
if(y) this.y=y;
}
Point.prototype.x=0;
Point.prototype.y=0;
function LineSegment(p1,p2){
var m_firstPoint=p1;
var m_lastPoint=p2;
var m_width={
valueOf:function(){return Math.abs(p1.x-p2.x)},
toString:function(){return Math.abs(p1.x-p2.x)}
}
var m_height={
valueOf:function(){return Math.abs(p1.y-p2.y)},
toString:function(){return Math.abs(p1.y-p1.y)}
}
this.getFirstPoint=function(){
function GETTER(){};
GETTER.prototype=m_firstPoint;
return new GETTER();
}
this.getLastPoint=function(){
function GETTER(){};
GETTER.prototype=m_lastPoint;
return new GETTER();
}
this.length={
valueOf:function(){return Math.sqrt(m_width*m_widht+m_height*m_height)},
toString:function(){return Math.sqrt(m_width*m_width+m_height*m_height)}
}
}
var p1=new Point();
var p2=new Point(2,3)
var line1=new LineSegment(p1,p2)
var lp=line1.getFirstPoint();
//改变了lp中得x属性,因此line1中得getFirstPoint的x属性也改变了。
lp.x=100;
alert(line1.getFirstPoint().x);
alert(line1.length);
讲一个对象设置为一个类型的原型,相当于通过实例化这个类型,为对象建立只读副本,在任何时候对副本进行改变,都不会影响到原始对象,而对原始对象进行改变,则会影响到每一个副本。除非被改变的类型已经被副本自己的同名属性覆盖,用delete操作将对象自己的同名属性删除,则可以恢复原型属性的可见性。
!注意的是原型得到的是一个具体的实例,而不是类。obj.prototype=new Object()
5.使用prototype定义静态方法。
prototype更常见的用处是声明对象的方法。因为在一般情况下,和属性相比,对象的方法不会轻易改变,正好利用prototype的静态特性来声明方法
这样避免了在构造函数中每次对方法进行重新赋值,节省了时间和空间。
function Point(x,y){
this.x=x;
this.y=y;
}
Point.prototype.distance=function(){
return Math.sqrt(this.x*this.x+this.y*this.y);
}
自然也可以用this.distance=function(){}的形式来定义Point对象的distance()方法,但是用了prototype避免了每次调用构造函数时对 this.distance的赋值操作和函数构造,如果程序里构造对象的次数很多的话,时间和空间的节省是非常明显的。
注意:尽量采用prototype定义对象方法,除非该方法要访问对象的私有成员或者返回某些引用的构造函数的上下文的闭包。
6.prototype的实质
将一个属性添加为prototype的属性,这个属性将被该类型创建的所有实例所共享,但是这种共享是只读的。在任何时候一个实例中只能够用自己的同名属性
覆盖这个属性,而不能改变它。换句话说,对象在读取某个属性时,总是先检查自身域的属性表,如果有这个属性,则会返回这个属性,否则就去读取prototype域,返回i
prototype域上得属性。另外javascript允许prototype域引用任何的对象,因此如果对prototype域的读取依然没有找到这个属性,则js将递归地查找prototype域所指向的prototype域,直到这个对象的prototype域为它本身或者出现循环为止。
function dwn(s){
document.write(s +"</br/>");
}
//定义了一个Point2D类
function Point2D(x,y){
this.x=x;
this.y=y;
}
//给Point2D设置原型属性
Point2D.prototype.x=0;
Point2D.prototype.y=0;
//生成ColorPoint2D类
function ColorPoint2D(x,y,c){
this.x=x;
this.y=y;
}
//ColorPoint2D类的原型为一个空的Point2D
//我发现如果你不给ColorPoint2D的原型赋值与Point2D的原型相同的属性的话,
//ColorPoint2D的对象无法调用Point中得原型属性,输出为undefined
ColorPoint2D.prototype=new Point2D();
var cp1=new ColorPoint2D(11);
//若前面没有定义ColorPoint2D的原型的x属性的话,那么这个就会输出undefi
delete cp1.x;
dwn(cp1.x);
delete ColorPoint2D.prototype.x;
//这个输出了Point2D的x值
dwn(cp1.x);
ColorPoint2D.prototype.x=1;
ColorPoint2D.prototype.y=1;
var cp=new ColorPoint2D(10,20,"red");
dwn(cp.x);
delete cp.x;
dwn(cp.x);
delete ColorPoint2D.prototype.x;
dwn(cp.x);
6.prototype的价值与局限性。
prototype可以通过它能够以一个对象为原型,安全地创建大量的实例,这就是prototype的真正含义,也是它的价值所在。可以利用这个特性模拟对象的继承。但是,,prototype用来模拟继承机关是它的一个重要价值,但是绝对不是它的核心,js之所以支持prototype,绝对不是仅仅用来实现它的对象继承,即使没有了prototype继承,js中得prototype机制依然是非常有用的。
由于prototype仅仅是以对象为原型给类型构建副本,因此它也具有很大的局限性,首先,它在类型的prototype域上并不是表现为一种值拷贝,而是一种引用拷贝,这就会带来副作用,改变某个原型上得引用类型的属性的属性值,将会彻底影响到这个类型创建的每一个实例。
总之,prototype是一种面向对象的机制,它通过原型来管理类型与对象之间的关系,prototype的特点是能够以某个类型为原型构造大量的对象。以prototype机制来模拟的继承是一种原型继承,它是js多种继承实现方式的一种,尽管prototype和传统的Class模式不同,但是我们依然可以认为prototype-based是一种纯粹的面向对象的机制。
7.继承
在js中并不直接从文法上支持继承,换句话说,js没有实现继承的语法,从这个意义上来说,js并不是直接的面向对象的语言。
7.1实现继承的方法。
要实现继承就是包含三层含义:1。子类的实例可以共享父类的方法2。子类可以覆盖父类的方法或者扩展新的方法3。子类和父类都是子类实例的类型。
7.2构造继承法
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
<script>
function dwn(s){
document.write(s+"<br/>");
}
//定义一个Collection类型
function Collection(size){
//这个公有方法可以被继承
this.size=function(){return size};
}
//这个原型方法不能在构造继承法中继承,可以在原型继承法的的时候继承
//但是注意原型的覆盖问题
Collection.prototype.isEmpty=function(){
return this.size()==0;
}
//定义一个ArrayList类型,它继承了Collection类型
function ArrayList(){
var m_elements=[];
m_elements=Array.apply(m_elements,arguments);
//这句是关键语法,用来继承的
this.base=Collection;
this.base.call(this,m_elements.length);
this.add=function(){
return m_elements.push.apply(m_elements,arguments);
}
this.toArray=function(){
return m_elements;
}
}
ArrayList.prototype.toString=function(){
return this.toArray().toString();
}
function SortedList(){
this.base=ArrayList;
this.base.apply(this,arguments);
this.sort=function(){
var arr=this.toArray();
arr.sort.apply(arr,arguments);
}
}
//构造一个ArrayList
var a=new ArrayList(1,2,3);
dwn(a);
dwn(a.size());//继承自Collection
dwn(a.isEmpty);
//构造一个SortedList
var b=new SortedList(3,1,2);
b.add(4,0);
dwn(b.toArray());//继承自ArrayList
b.sort();
dwn(b.toArray());
dwn(b);
dwn(b.size());
</script>
</head>
<body>
</body>
</html>
构造继承法重点是在,方法体内先定义一个公有属性一般为this.base然后把指定的父类给他this.base=Collection
接着就是把自身传入给父类的构造方法,同时根据情况传递参数!
this.base.call(this,arguments)
从继承关系上看,ArrayList继承了Collection公有的size()方法,但是却无法继承Collection静态的isEmpty()方法。ArrayList定义了自己的add()方法。SortedList继承了ArrayList的add()方法,以及从Collection继承下来的size()方法,但是却不能继承ArrayList重写的静态toString()方法。SortList定义了自己的sort()方法。注意到SortedList的size()方法实际上有一个BUG,用add()方法添加了元素之后并没变更size()的值,这是因为Collection类中定义的size()返回的是外部环境的参数,它不会受到子类ArrayList与SortedList的影响,所以要维持size的正确性,这能在ArrayList中重写size()方法。如下:
this.size=function(){return m_elements.length}
注意到实际上构造继承法,并不能满足继承的第三层含义,无论是a instanceof Collection 还是b instanceof ArrayList返回值总是false。其实这种继承方法除了通过调用父类构造函数将属性复制得到自身之外,并没有其他任何的事情。严格来说,它甚至算不上继承。尽管如此用它的特性来模拟常规的对象继承,也基本上达到了我们预期的目标。这种方法的优点是简单和直观,而且可以自由地用灵活的参数执行父类的构造函数,通过执行多个父类构造函数方便地实现多重继承,缺点主要是不能够继承静态属性和方法,也不能满足所有父类都是子类实例的类型这个条件。这样对于实现多态会造成麻烦。
7.3原型继承法
原型继承法是js中最流行的一种继承方式,以至于有人数,js的面向对象机制实际上是基于原型的一种机制,或者说,js是一种基于原型的语言。
基于原型编程是面向对象编程的一种特定形式,在这种基于原型的编程模式中,不是通过声明静态的类,而是通过复制已经存在的原型对象来实现行为重用。这个模型一般被称为clas-less,面向原型,或者是基于接口编程。
既然如此,基于元习惯模型其实并没有类的概念,这里所说的类是一种模拟,或者说是沿用了传统的面向对象编程的概念。所以,这种原型继承法和传统的类继承法并不一致。
原型继承法虽然有着类继承无法比拟的优点,也有缺点,一个很大的缺陷就是前面你所说的prototype的副作用。
prototype的最大特点是能够让对象实例共享原型对象的属性,因此如果把某个对象作为一个类型的原型,那么我们说这个类型的所有实例以这个对象为原型。这个时候,实际上这个对象的类型也可以作为那些以这个对象为原型的实例的类型。
function dwn(s){
document.write(s+"<br/>");
}
function Point(dimension){
this.dimension=dimension;
}
//定义一个Point2D类型,继承Point类型
function Point2D(x,y){
this.x=x;
this.y=y;
}
Point2D.prototype.distance=function(){
return Math.sqrt(this.x*this.x+this.y*this.y);
}
//这里继承了Point,说明有2个点
Point2D.prototype=new Point(2)
//定义一个Point3D类型,也继承了Point类型
function Point3D(x,y,z){
this.x=x;
this.y=y;
this.z=z;
}
Point3D.prototype=new Point(3);
var p1=new Point2D(0,0);
var p2=new Point3D(0,1,2);
dwn(p1.dimension);
dwn(p2.dimension);
dwn(p1 instanceof Point2D);
dwn(p1 instanceof Point);
dwn(p2 instanceof Point);
在这个简单的例子里,Point2D和Point3D都是以Point为原型,Point为Point2D和Point3D提供dimension属性。从面向对象的角度,相当于Point2D和Point3D都继承了Point,有趣的是,p1和p2虽然分别是Point2D和Point3D的实例,但是p1 instanceof Point和 p2 instanceof Point(父类)的值都是true,这样就可以实现多态了。
类型的原型可以构成一个原型链,这样就能够实现多个层次的继承。继承链上得米一个对象都是实例的类型。
同构造继承法相比,原型继承法的优点是结构更加简单,而且不需要每次构造都调用父类的构造函数,尽管你仍然可以调用它,并且不需要通过复制属性的方式就能快速实现继承。但是它的缺点也很明显,首先它不方便直接支持多重继承,因为一个类型只能有一个原型;其次它不能很好的支持多参数和动态参数的父类构造,因为每个原型继承阶段你都必须把参数固定下来,以实例化一个父类对象作为当前类型的原型,有时候父类对象是不能也不应该随便实例化的;最后一个缺点就是之前提到过得prototype的副作用。
但是我么依然应该使用它。因为,同类继承相比,原型继承本来就是一个简化了的版本,因此我们不应该要求它完全达到标准的类继承的效果,实际上当你的父类是一个简单、抽象的模型或者接口的时候,原型继承的表现在已知的js对象继承中是最好的,甚至可以说,prototype继承才是js文法上提供的真正意义上得继承机制,所以,我们在使用js时,能够采用原型继承的地方,应当尽可能地采用这种继承方式。
7.3实例继承法
构造继承法与原型继承法各有一个明显的缺点前面没有具体提到,由于构造继承法没有办法继承类型的静态方法,因此它无法很好地及继承js的核心对象。而原型继承法虽然可以继承静态方法,但是依然无法很好地继承核心对象中得不可枚举方法。
构造函数通常没有返回值,他们只是初始化由this值传递进来的对象,并且什么也不返回。如果函数有返回值,被返回的对象就成了new表达式的值。这句话引出了一个新的继承方法,实例继承法。
function dwn(s){
document.write(s+"<br/>");
}
function MyDate(){
var instance=new Date();
instance.printDate=function(){
document.write("<p>"+instance.toLocaleDateString()+"</p>");
}
return instance;
}
var myDate=new MyDate();
dwn(myDate.toGMTString());
myDate.printDate();
它确实继承了核心对象的Date的方法。通常情况下要对js原生核心对象或者DOm对象进行继承时,我们会采用这种继承方法。不过它也有几个明显的缺点,首先由于它需要在执行构造函数的时候构造基类的对象,而js的new运算符与函数调用不同的是不能用apply()方法传递给它不确定的arguments集合,这样就会对那些可以接受不同类型和不同数量参数的类型的继承造成比较大的麻烦。
其次,这种继承方式是通过在类型中构造对象并返回的方法来实现继承的,那样的话new运算的结果实际上是类型中构造的对象而不是类型本身创建的对象。alert(myDate instanceof MyDate)的执行结果将会是fasle,对象的构造函数将会是实际构造的对象的构造函数而不是类型本身的构造函数,尽管你可以通过赋值的办法修正它,但是你却无法修正instanceof表达式的结果。
第三,这种方法一次只能返回一个对象,它和原型继承法一样不能支持多重继承。
所以:构造继承法也不是一钟真正的继承法,它也是一种模拟,构造继承法是目前所知的唯一一种可以较好地继承js核心对象的继承法。当你要继承js的核心对象或者Dom对象时,可以考虑这种方法。
7.4拷贝继承法
拷贝继承法就是通过对象属性的拷贝来实现继承,早期的prototype和其他一些框架在特定的情况下就用到了这种继承法。
Function.prototype.extends=function(obj){
for(var each in obj){
this.prototype[each]=obj[each]
}
}
拷贝继承法实际上是通过反射机制拷贝基类对象的所有可枚举属性和方法来模拟“继承”,因为可以拷贝任意数量的对象,因此它可以模拟多继承,又因为反射可以枚举对象的静态属性和方法,所以它同构造继承法相比的优点是可以继承父类的静态方法。但是由于是反射机制,因此拷贝继承法不能继承非枚举类型方法,例如父类中重载的toString()方法,另外拷贝继承法也有几个明显的缺点,首先是通过反射机制来复制对象属性效率上十分低下,其次它也要构造对象,通常也不能很好地支持灵活的可变参数。第三如果父类的静态属性中包含引用类型,它和圆形继承法一样导致副作用。第四,当前类型如果有静态属性,这些属性可能会被父类的动态属性所覆盖,最后这种可支持多重继承的方式并不能清晰地描述出父类与子类的相关性。
7.5混合继承法
混合继承法是将两种或者两种以上的继承同时使用,其中最常见的构造继承和原型继承混合使用,这样能够解决构造函数多参数多重继承的问题。
function Point2D(x,y){
this.x=x;
this.y=y;
}
funtion ColorPoint2D(x,y,c){
Point2D.call(this.x,y);
this.color=c;
}
ColorPoint2D.prototype=new Point2D();