第六章 面向对象的程序设计
面向对象(Object-Oriented,OO)
ECMAscript把对象定义为:无序属性的集合,其属性可包含基本值,对象或函数。
可以把其中的对象想象成一组组的名值对,其中值可以是数据或函数。
每个对象都是基于一个引用类型创建的。
可以new创建一个对象,也可以使用对象字面量。
var person=new Object();
person.name="zhaosi";
person.sayname=function(){
alert(this.name);
};
var person={
name:"zhaosi",
age:29,
sayname:function(){
alert(this.name);
}
};
1、对象具有属性(property)和方法,对象的属性里有内部才有的特性(attribute),这个值一般不能直接访问,用[[]]双方括号来放置这些特性。用来修改默认配置,比如可不可以修改某一属性值。
2、创建对象
虽然构造函数和对象字面量都可以创建一个对象,但是这些方式有个缺点:使用同一个接口创建很多对象,会产生大量重复的代码。为解决这个问题,使用了以下几个方式:
工厂模式
抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。
function createPerson(name){
var o=new Object();
o.name=name;
o.sayName=function(){
alert(this.name);
};
return o;
}
var person=createPerson("zhaosi");
虽然解决了创建多个相似对象的问题,却没有解决对象识别的问题(即怎样知道一个对象的类型)
构造函数模式
构造函数可用来创建特定类型的对象,如原生的Object、Array这样的构造函数。也可以创建自定义的构造函数,自定义对象的属性与方法。
function Person(name){
this.name=name;
this.sayName=function(){
alert(this.name);
};
}
var person1=new Person("zhaosi");
var person2=new Person("zhangsan");
注意:构造函数首字母大写
创建自定义构造函数意味着将来可以将它的实例标识为一种特定的类型。如上面的person对象类型为Person。
使用构造函数的缺点:每个方法都要在每个实例上重新创建一遍。因为函数方法本身也是对象,每定义一个函数就是实例化一个对象!(person1.sayName!=person2.sayName)
如果把function定义到构造函数外面来,就只会构造一个函数对象,也可以进行调用。但是这样做的问题是,如果有多个函数就要定义很多个全局函数,这个自定义引用类型就没有封装性可言。
function Person(name){
this.name=name;
this.sayName=sayName;
}
function sayName(){
alert(this.name);
}
好在我们可以使用原型模式解决这个问题。
*原型模式
我们创建的每一个函数都有一个原型(prototype)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person(){}
Person.prototype.name="zhaosi";
Person.prototype.sayName=function(){
alert(this.name);
}
var person1=new Person();
var person2=new Person();
person1.sayName(); //"zhaosi"
person2.sayName(); //"zhaosi"
此时的person1.sayName==person2.sayName。因为共享属性和方法,访问的都是同一个函数、同一个属性。
理解原型对象
无论何时只要创建一个函数就会有prototype属性,指向函数原型对象。原型对象有constructor(构造函数)属性,指向prototype属性所在函数。如:Person.prototype.constructor指向Person。
在实例中复写属性会屏蔽原型属性。删除实例中的属性后仍然可以访问原型属性。
isPrototypeOf判断某原型是否是某实例的原型。
Object.getPrototypeOf(实例) 这个函数得到原型(ES5)
hasOwnProperty检测一个属性是否存在于实例中。
in 操作符,只要属性在实例或原型中,返回true,”name” in person1 //true
for 属性 in 对象,枚举不光是实例中也包括原型中的所有定义的属性。
ES5中的Object.keys()方法返回的则是属性字符串数组,是实例就枚举实例属性,是原型就枚举原型属性
原型也可以用字面量来写。
Person.prototype={
name:"zhaosi",
age:29,
sayname:function(){
alert(this.name);
}
}
原型的动态性:对原型对象做出任何修改都会立刻对实例产生影响。但重写原型是不会的。
原生对象的原型中也定义了很多方法,也可以修改,增加。比如String,可以为其原型增加一个方法,以后可以直接调用String.方法。
原型对象最大的问题来源于其共享性和动态性,一些本不该共享的属性被共享被修改会出现一些不必要的麻烦。
原型模式与构造模式组合使用可以避免上述问题。需要共享的放在原型中,一些私有的放在自己的构造函数里
function Person(name){
this.name=name;
this.sayName=function(){
alert(this.name);
};
}
Person.prototype={
constructor:Person;
sayname:function(){
alert(this.name);
}
}
另一种完美的方法是动态原型模式,将所有信息都封装在构造函数中,在构造函数中初始化原型。
function Person(name){
this.name=name;
if(typeof this.sayName != "function"){
Person.prototype.sayName=function(){
alert(this.name);
};
}
}
还有寄生构造函数模式、稳妥构造函数模式(没细看了 - -)
3、继承(实现继承)
依靠原型链,原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
如果让原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
实现原型链的基本模式:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = false;
}
// 继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); // true
画图更易于理解。
注意点:
默认原型是Object,因此对象才有了基本的几种方法。
确定原型与实例的关系:instanceof和isPrototypeOf
给原型添加方法的代码一定要放在替换原型的语句之后。
在通过原型链实现继承时,不能使用字面量创建方法,那样会重写原型链!
问题:
最主要的问题来自于包含引用类型值的原型。原型会被所有实例共享,通过原型继承时,原型会变成另一个类型的实例,于是原先的实例属性变成了原型属性,也会被共享。
还有一个问题是不能向超类型的构造函数中传递参数。
借用构造函数:思想:在子类型构造函数内部调用超类型构造函数(call、apply)
组合继承:原型链+借用构造函数。
原型式继承、寄生式继承、寄生组合式继承(- -)