面向对象与原型
ECMAScript有两种开发模式:1.函数式(过程化),2.面向对象(OOP)。面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
面向过程就是你把代码封装成函数(procedure),然后依次去做一件事情;
面向对象就是你把要做的事情抽象成对象,告诉对象去做。
面向对象三大特性(封装,继承,多态)使得在做复杂的事情的时候效率和正确率得到保证
创建对象
1,用new关键字,创建一个对象,然后给这个对象新建属性和方法。
var box = new Object();//创建一个Object对象
box.name = 'xiaoming';//创建一个name属性并赋值
box.age = 18;//创建一个age属性并赋值
box.run = function () {//创建一个run()方法并返回值
return this.name + this.age + '运行中...';
};
alert(box.run());//输出属性和方法的值xiaoming18运行中
上面创建了一个对象,并且创建属性和方法,在run()方法里的this,就是代表box对象本身。这种是JavaScript创建对象最基本的方法,但有个缺点,想创建一个类似的对象,就会产生大量的代码。
var box2 = box;//得到box的引用
box2.name = 'Jack';//直接改变了name属性
alert(box2.run());//用box.run()发现name也改变了Jack18运行中
alert(box.run());//两者的值一样,混淆了
var box2 = new Object();
box2.name = 'Jack';
box2.age = 200;
box2.run = function () {
return this.name + this.age + '运行中...';
};
alert(box2.run());//这样才避免和box混淆,从而保持独立
2,字面量的方式创建对象 即json方式
var obj2={
"name":"xiaoming",
"age":18,
"sing":function(){
return "我叫"+this.name+",今年"+this.age+"岁了";
} };
虽然json的方式也可以定义对象,但是它和new Object一样存在了不能对象重用的缺陷,所以大家研究出了一种工厂方式来定义一个对象,下面我们来使用工厂方式来实现一个对象的定义。
3,利用工厂模式创建对象
functioncreatObj(name,sex){//传参的方式,可以大批量的创建不同对象
varobj=newObject();
obj.name=name;
obj.sex=sex;
obj.sing=function(){
returnthis.name+"===>"+this.sex;
}
return obj; }
varp1=creatObj("小明","男");
alert(p1.sing());//小明的信息
varp2=creatObj("小花","女");
alert(p2.sing());//小花的信息
alert(typeofp1);//object 仅仅能识别p1只是一个对象 没办法进行类型的辨析
//工厂模式解决了对象无法重用的问题,但是无法判断对象的类型
我们使用了工厂模式定义了对象,这样就很好的解决了对象无法重用的问题,但是此时又存在了另一个问题,就是我们无法判断得到的对象的类型了,如typeof或者instanceof来判断类型,仅仅得到一个Object类型。
4.构造函数的方式创建对象
ECMAScript中可以采用构造函数(构造方法)可用来创建特定的对象。类型于Object对象。
function Box(name, age) {//构造函数模式
this.name = name;
this.age = age;
this.run = function () {
return this.name + this.age + '运行中...';
}; }
ar box1 = new Box(xiaoming', 100);//new Box()即可 类的实例化,创建了对象实例
var box2 = new Box('Jack', 200);
alert(box1.run());
alert(box1 instanceof Box);//true很清晰的识别他从属于Box
instanceof只能用来判断对象和函数,不能用来判断字符串和数字等
使用构造函数的方法,既解决了重复实例化的问题,又解决了对象识别的问题
构造函数的方法有一些规范:
1.函数名和实例化构造名相同且大写,(PS:非强制,但这么写有助于区分构造函数和普通函数);
2.通过构造函数创建对象,必须使用new运算符。
既然通过构造函数可以创建对象,那么这个对象是哪里来的,new Object()在什么地方执行了?执行的过程如下:
1.当使用了构造函数,并且new构造函数(),那么就后台执行了new Object();
2.将构造函数的作用域给新对象,(即new Object()创建出的对象),而函数体内的this就代表new Object()出来的对象。
3.执行构造函数内的代码;
4.返回新对象(后台直接返回)。
此时我们发现基于构造函数的定义对象的方式看似已经很完美了,我们需要的问题它都可以解决了,但是如果我们仔细的分析这段代码的话,就会发现这样的代码是存在问题的,为什么呢?
我们通过代码分析得知:run方法在每个对象创建后都存在了一个方法拷贝(但是我们发现代码在只有调用时,run方法才会在堆中创建),这样就增加了内存的消耗了(闭包),如果在对象中有大量的方法时,内存的消耗就会高,这样不行了。
function Person(name,sex){//函数名首字母记得大写
this.name=name;
this.sex=sex;
var a=12;//局部变量a
window.a=18;//window对象是a
this.say=say;//方法引用
}
function say(){
//全局的方法 这样每次调用的时候,用完了后,会直接销毁该函数,不会占用内存
alert(this.name+"===>"+this.sex);}
//但是这用方法虽然解决了上述问题,但又出现了新的问题,就是破坏了对象的封装性
构造函数方式var p1=new Person("夏明","男");//类的实例化,创建了对象实例
var p2=new Person("小花","女");
p1.say();
alert(p1.name);//夏明
alert(p1.a);//undefined
alert(a);//18 window.a
创建原型对象
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype通过调用构造函数而创建的那个对象的原型对象。
使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
functionPerson(){}//声明构造函数
Person.prototype.name="xiaoming";//在原型里添加属性
Person.prototype.sex="男";
Person.prototype.sing=function(){//在原型里添加方法
alert(this.name);//this指向实例对象 原型对象
}
varp=newPerson();//p是Person的实例对象 指向原型对象
//p.sing();// xiaoming
//alert(p.name);//xiaoming
varp2=newPerson();
//如果给p2的属性和方法重新赋值,会首先输出重新赋的值,如果没有重新赋值,会输出原型里的值
p2.name="jack";
p2.sing=function(){
alert(this.name);//this指向p2实例对象
}
p2.sing();//jack
alert(p2.sex);//男 指向原型里的值
为了更进一步了解构造函数的声明方式和原型模式的声明方式,我们通过图示来了解一下:
原型模式方式在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性constructor。通过这两个属性,就可以访问到原型里的属性和方法了。
PS:IE浏览器在脚本访问__proto__会不能识别,火狐和谷歌浏览器及其他某些浏览器均能识别。虽然可以输出,但无法获取内部信息。
alert(box1.__proto__);//[object Object]
原型检测方式:
1).isPrototypeOf()
判断一个对象是否指向了该构造函数的原型对象,可以使用isPrototypeOf()方法来测试。
alert(Box.prototype.isPrototypeOf(box));//只要实例化对象,即都会指向
原型模式的执行流程:
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
var box1 = new Box();
alert(box1.name);//原型里的值
box1.name = 'Jack';
alert(box1. name);//Jack,就近原则,
var box2 = new Box();
alert(box2.name);//原型里的值,没有被box1修改
如果想要box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可,具体如下:
delete box1.name;//删除属性delete只能删除自己的,不能删除原型里的属性
alert(box1.name);//原型里的name值
2).hasOwnProperty()
如何判断属性是在构造函数的实例里,还是在原型里?可以使用hasOwnProperty()函数来验证:
构造函数实例属性和原型属性示意图alert(box.hasOwnProperty('name'));//实例里有返回true,否则返回false
3).in操作符
in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
alert('name' in box);//true,存在实例中或原型中
我们可以通过hasOwnProperty()方法检测属性是否存在实例中,也可以通过in来判断实例或原型中是否存在属性。那么结合这两种方法,可以判断原型中是否存在属性。
function isProperty(object, property) {//判断原型中是否存在属性
return !object.hasOwnProperty(property) && (property in object);
}
或者:
functionisProperty(obj,prop){
if(!obj.hasOwnProperty(prop)){
if(propinobj){
returntrue;
} }
returnfalse; }
var box = new Box();
alert(isProperty(box, 'name'))//true,如果原型有
3.原型重写(覆盖)
为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
function Box() {};
Box.prototype = {//使用字面量的方式
name : 'xiaoming',
age : 100,
run : function () {
return this.name + this.age + '运行中...';
}
};
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数创建的方式则相反。
var box = new Box();
alert(box instanceof Box);
alert(box instanceof Object);
alert(box.constructor == Box);//字面量方式,返回false,否则,true
alert(box.constructor == Object);//字面量方式,返回true,否则,false
如果想让字面量方式的constructor指向实例对象,那么可以这么做:
Box.prototype = {
constructor : Box,//直接强制指向即可
};
PS:字面量方式为什么constructor会指向Object?因为Box.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它prototype,这个对象也会自动获取constructor属性。所以,新对象的constructor重写了Box原来的constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为Object。
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。
function Box() {};
Box.prototype = {//原型被重写了
constructor : Box,
name : 'Zeng',
age : 100,
run : function () {
return this.name + this.age + '运行中...';
}
};
Box.prototype = {
age = 200
};
var box = new Box();//在这里声明
alert(box.run());//box只是最初声明的原型
原型对象不仅仅可以在自定义对象的情况下使用,而ECMAScript内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。
alert(Array.prototype.sort);//sort就是Array类型的原型方法
alert(String.prototype.substring);//substring就是String类型的原型方法
String.prototype.addstring = function () {//给String类型添加一个方法
return this + ',被添加了!';//this代表调用的字符串
};
alert('Zeng'.addstring());//使用这个方法
PS:尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
4.原型对象出现的问题
原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。
原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:
function Box() {};
Box.prototype = {
constructor : Box,
name : '小明',
age : 10,
family : ['父亲', '母亲', '妹妹'],//添加了一个数组属性
run : function () {
return this.name + this.age + this.family;
}
};
var box1 = new Box();
box1.family.push('哥哥');//在实例中添加'哥哥'
alert(box1.run());
var box2 = new Box();
alert(box2.run());//共享带来的麻烦,也有'哥哥'了
PS:数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要保留自己的特性,而不能共享。
5.组合模式
为了解决构造传参和共享问题,可以组合构造函数+原型模式:
属性在构造函数中定义,将方法在原型中定义。这种有效集合了两者的优点,是目前最为常用的一种方式
function Box(name, age) {//不共享的使用构造函数
this.name = name;
this.age = age;
this. family = ['父亲', '母亲', '妹妹'];
};
Box.prototype = {//共享的使用原型模式
constructor : Box,
run : function () {
return this.name + this.age + this.family;
}
};
PS:这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。