面向对象
JavaScript中没有类的概念,因此JS的对象和基于类的语言中的对象不完全相同。
1 理解对象
如下两种方法都可以创建对象:
//方法1——构造函数
var person = new Object();
person.name = "Name";
person.age = 20;
person.sayName = function(){
alert(this.name);
};
//方法2——字面量语法
var person = {
name : "Name";
age : 20;
sayName : function(){
alert(this.name);
}
};
1.1 属性类型
属性拥有一些用于和JS引擎打交道的特性,在JS中不能直接访问,通常用两对方括号包围,例如[[Writable]]。
JS中有两种属性:数据属性和访问器属性。
1.1.1 数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值。
数据属性有4个特性:
特性 | 说明 |
---|---|
[[Configurable]] | true表示该属性可以通过delete删除并重新定义,可以修改属性的特性,可以把属性修改为访问器属性。直接在对象上定义的属性,默认为true。 |
[[Enumerable]] | true表示该属性可以通过for-in循环返回。直接在对象上定义的属性,默认为true。 |
[[Writable]] | true表示该属性的值可以修改。直接在对象上定义的属性,默认为true。 |
[[Value]] | 包含了该属性的数据值。读取该属性的时候从这个位置读,写入的时候,新值保存在这个位置。默认为undefined。 |
要修改属性默认的特性,必须使用Object.defineProperty()
方法。
Object.defineProperty(objectName,propertyName,descriptor)
参数 说明 objectName 属性所在的对象名 propertyName 要操作的属性名 descriptor 要操作的特性,取值为4个特性的一个或多个,用散列表包装。
如果操作将Configurable特性改为false,则再也无法改变为true。
1.1.2 访问器属性
访问器属性不包含数据值,而是包含一对getter和setter函数(这两个函数不是必需的)。当读取访问器属性的时候,会调用getter函数,负责返回有效的值,再写入访问器属性时,会调用setter函数并传入新值,负责决定如何处理数据。
访问器属性有4个特性:
特性 | 说明 |
---|---|
[[Configurable]] | true表示该属性可以通过delete删除并重新定义,可以修改属性的特性,可以把属性修改为数据属性。直接在对象上定义的属性,默认为true。 |
[[Enumerable]] | true表示该属性可以通过for-in循环返回。直接在对象上定义的属性,默认为true。 |
[[Get]] | 在读取属性时调用的函数。默认为undefined。 |
[[Set]] | 在写入属性时调用的函数。缺省表示该属性不能写。默认为undefined。 |
访问器属性不能直接定义,必须使用Object.defineProperty()
方法。
var book = {
_year : 2016; //下划线表示这是只能通过对象方法访问的属性
edition : 1
}; //定义一个很普通的对象
Object.defineProperty(book,"year",{
get : function(){
return this._year;
},
set : function(newValue){
if (newValue > 2016){
this._year = newValue;
this.edition += newValue - 2016;
}
}
});//设置book对象的year属性的get和set函数
对于不支持Object.defineProperty()
方法的浏览器,可以使用__defineGetter__()
和__defineSetter__()
替代:
var book = {
_year : 2016; //下划线表示这是只能通过对象方法访问的属性
edition : 1
}; //定义一个很普通的对象
book.__defineGetter__("year",function(){
return this._year;
});//设置book对象的year属性的get函数
book.__defineSetter__("year",function(newValue){
if (newValue > 2016){
this._year = newValue;
this.edition += newValue - 2016;
}
});//设置book对象的year属性的set函数
1.2 定义多个属性
对象往往不止一个属性,对于多个属性的定义可以使用Object.defineProperties()
方法。
Object.defineProperties(object,properties)
参数 说明 object 要添加和修改其属性的对象 properties 与第一个参数的对象中要添加或修改的属性一一对应的散列表
举个例子:
var book = {};
Object.defineProperties(book,{
_year : {
value : 2016
},
edition : {
value : 1
},
year : {
get : function(){
return this._year;
},
set : function(newValue){
if (newValue > 2016){
this._year = newValue;
this.edition += newValue - 2016;
}
}
}
});
1.3 读取属性的特性
Object.getOwnPropertyDescriptor()
取得给定属性的特性。
Object.getOwnPropertyDescriptor(object,propertyName)
参数 说明 object 属性所在的对象 propertyName 属性名 返回
包含该属性所有特性的对象
对于之前的例子:
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value); //2016
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book,"year");
alert(descriptor.value); //"undefined"
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
2 创建对象
构造函数和对象字面量的缺点是使用同一个接口创建多个对象时会产生大量重复代码。
2.1 工厂模式
工厂模式——用函数来封装以特定接口创建对象的细节。
//工厂模式举例
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Andy",20,"student");
var person2 = createPerson("Bob",30,"doctor");
工厂模式解决了代码重复的问题,但没有解决识别对象的问题,无法知道一个对象是什么类型。
2.2 构造函数模式
JS可以创建自定义的构造函数,从而定义对象类型的属性和方法。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){alert(this.name);};
}
var person1 = new Person("Andy",20,"student");
var person2 = new Person("Bob",30,"doctor");
用Person()函数替代createPerson()函数,这个方法有是哪个特点:
- 没有显示地创建对象
- 直接将属性和方法赋给了this对象
- 没有return
这种方法实际上经历了以下几步:
- 创建一个新对象
- 将构造函数作用域赋给新对象,因此this就指向了这个新对象
- 执行构造函数中的代码,为这个新对象添加属性
- 返回新对象
上例中的person1和person2分别保存了Person的两个实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person。constructor属性最初是用来标识对象类型的。这个例子创建的对象既是Object的实例也是Person实例。
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
与工厂模式相比,自定义构造函数意味着将来可以将它的实例标识为一种特定的类型
但构造函数也有缺点,每个方法都要在每个实例上重新创建一遍。
2.3 原型模式
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,该对象的用途是包含可以有特定类型的所有实例共享的属性和方法。即prototype就是通过调用构造函数而创建的那个对象实例的原型对象。
其优点是可以让所有对象实例共享他所包含的属性和方法,不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中。这种方法同一对象的实例访问的都是同一组属性和同一个方法。
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。所有原型对象都会获得一个constructor属性,包含一个指向prototype的指针。上例中Person.prototype.constructor指向Person。通过这个函数可以继续为原型对象添加其他属性和方法。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的那个同名属性,delete操作符删掉实例的属性值可以解除屏蔽,对实例的修改不会影响原型。
hasOwnProperty(propertyName)方法可以确定什么时候访问的是实例属性,什么时候访问的原型属性。只有实例重写了属性名才会返回true。
in操作符,有两种使用方法——单独使用和for-in循环。
in操作符会在通过对象能够访问给定属性时返回true,无论该属性在实例中还是原型中。
function Person(){
}
Person.prototype.name = "Andy";
Person.prototype.age = 20;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false 没有重写name
alert("name" in person1); //true
person1,name = "Grey";
alert(person1.name); //"Grey"来自实例
alert(person1.hasOwnProperty("name")); //true 重写了name
alert("name" in person1); //true
alert(person2.name); //"Andy"来自原型
alert(person2.hasOwnProperty("name")); //false 没有重写name
alert("name" in person2); //true
delete person1.name; //删除person1的name属性
alert(person1.name); //"Andy"来自原型
alert(person1.hasOwnProperty("name")); //false 没有重写name
alert("name" in person1); //true
组合使用二者,
当in返回true,hasOwnProperty()返回false,表示属性存在于原型
当in返回true,hasOwnProperty()返回true,表示属性存在于实例
for-in循环时,返回的是所有能够通过对象访问的,可以枚举的属性,既包括实例中的也包括原型中的。
要取得对象上所有的可枚举实例属性,可以使用Object.keys()
方法:
// 接上例
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,sayName"
var p1 = new Person();
p1.name = "Bob";
p1.age = 100;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
要取得所有实例属性,无论是否可以枚举,可以使用Object.getOwnPropertyNames()
方法。
尽管可以随时为原型添加属性和方法,并且修改可以立即在所有对象实例中反映出来,但是如果重写整个原型对象,则相当于把原型修改为另外一个对象,也就切断了构造函数和最初原型之间的关系。实例中的指针指向原型,而不是构造函数
2.3.1 原生对象的原型
原生的引用类型也是采用的这种模式创建的。可以像修改自定义对象的原型一样修改原生对象的原型。
String.prototype.startWith = function(text){
return this.indexOf(text) == 0;
};
var msg = "My name is Andy.";
alert(msg.startWith("My")); //true
上例,为String引用类型添加了一个startWith()方法。所有当前环境下的字符串都可以调用这个方法。
2.3.2 原型对象的缺点
原型中的所有属性是被很多实例共享的,这种共享对于函数是有利的,对于基本类型的属性,也可以通过赋新值来改变,但是对于引用类型的属性,多个实例的操作都会反映在一个原型上,导致每个实例之间不是相互独立的。
如下例:
function Person(){
}
Person.prototype = {
name : "Andy";
age : 20;
friends : ["Bob","Candy"];
sayName = function(){
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Doggy"); //对person1实例修改
alert(person1.friends); //"Bob,Candy,Doggy"
alert(person2.friends); //"Bob,Candy,Doggy" 对person1的修改对person2产生了影响
alert(person1.friends == person2.friends); //true
2.4 构造函数模式与原型模式组合
用构造函数模式定义实例属性,用原型模式定义方法和共享的属性。
这样每个实例都有独立的实例属性的空间,又共享了方法。
还是同样的例子:
function Person(name,age){
this.name = name;
this,age = age;
this.friends = ["Bob","Candy"];
}//用构造函数模式定义属性
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}//用原型模式定义方法
var person1 = new Person("Andy",20);
var person2 = new Person("Eric",2);
person1.friends.push("Doggy");
alert(person1.friends); //"Bob,Candy,Doggy"
alert(person2.friends); //"Bob,Candy" 不受到person1的影响
alert(person1.friends === person2.friends); //false 属性各自独立
alert(person1.sayName === person2.sayName); //true 共享方法
2.5 动态原型模式
上述方法仍然不够简洁和美观,动态原型模式把所有信息都封装在构造函数中,通过在构造函数中初始化原型,又保持了同时使用二者的优点。通过检查某个应该存在的方法是否有效来决定是否需要初始化原型。
fucntion Person(name,age){
this.name = name;
this.age = age;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
2.6 寄生构造函数模式
原理工厂模式其实是一样的,一个函数创建一个对象,并给该对象赋予属性和方法,再将这个对象返回。唯一的不同,是使用new操作符新建对象,把工厂函数成为构造函数而已。
//在不修改Array构造函数的情况下,创建一个具有特殊方法的数组
function SpecialArray(){
var values = new Array();//创建一个普通的数组
values.push.apply(values,arguments); //给对象添加值
values.toPopeString = function(){
return this.join("|");
};//给对象添加方法
return values;
}
var colors = new SpecialArray("red","blue","green");
alert(colors.toPipeString); //"red|blue|green"
3 继承
同其他面向对象语言一样,JS也有继承。
JS的继承主要依靠原型链来实现。
3.1 原型链
这个概念很重要!!!
基本思想是利用原型让每一个引用类型继承另一个引用类型的属性和方法。
- 每个构造函数都有一个原型对象
- 每个原型对象都包含一个指向构造函数的指针
- 每个实例都包含一个指向原型对象的内部指针
如果让一个原型对象等于另一个类型的实例,则原型对象将包含一个指向另一个原型的指针,相应的,另一个原型也包含一个指向另一个构造函数的指针,如果另一个原型又是另一个类型的实例,同样满足上述关系,层层递进,成了一个“链”。
实现原型链的基本模式:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType(); //SubType继承了SuperType
SubType.prototype.getSubValue = function(){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
继承的关键一步是新建的SuperType的实例赋给SubType的原型,其本质是重写了SubType的原型对象,所以原来存在于SuperType的实例中的所有属性和方法现在也存在于SubType.prototype中。
下图较为直观的表现了这二者的关系。
最终结果就是SubType的实例instance指向SubType的原型,SubType的原型又指向SuperType的原型。getSuperValue()方法作为原型方法还在SuperType.prototype中,property作为实例属性已经位于SubType.prototype中。
原型搜索机制——当读取模式访问一个实例属性是,首先会在实例中搜索该属性,如果没有,则会继续搜索实例的原型。
原型链扩展了这种搜索机制,当在原型中没有找到,则会沿着原型链继续向上搜索。
上例中instance.getSuperValue()会经历三个步骤:
- 搜索instance
- 搜索SubType.prototype
- 搜索SuperType.prototype
3.1.1 默认的原型
每个引用类型都默认继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object实例,因此默认原型都会包含一个内部指针指向Object.prototype。
3.1.2 确定原型和实例的关系
有两种方法:
- instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,就返回true
- isPrototypeOf()方法,只要是原型链中出现过的原型,就返回true
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3.1.3 谨慎的定义方法
子类型重写父类型的方法或添加父类没有的方法时,相关的代码要放在替换原型的语句之后。
function SuperType(){
this.property = true;
}//父类
SuperType.prototype.getSuperValue = function(){
return this.property;
};//父类方法
function SubType(){
this.subproperty = false;
}//子类
SubType.prototype = new SuperType(); //SubType继承了SuperType
SubType.prototype.getSubValue = function(){
return this.subproperty;
};//添加新方法
SubType.prototype.getSuperValue = function(){
return false;
};//重写父类方法
var instance = new SubType();
alert(instance.getSuperValue()); //false 调用的是重写后的getSuperValue()
特别重要的一点是,原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链,导致继承失效。
3.1.4 原型链的缺点
和原型对象的缺点一样,原型链的问题来自包含引用类型值的原型。问题还是出在共享属性上。
3.2 借用构造函数
为了解决上述问题,提出了一种借用构造函数。
即在子类型构造函数的内部调用父类型构造函数。
function SuperType(){
this.colors = ["red","blue","green"];
}
function SubType(){
//继承了SuperType
SuperType.call(this); //在子类的构造函数中调用父类的构造函数
}
var instance1 = new Subtype();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green" 两个实例相互独立 属性不影响
3.2.1 传递参数
相对于原型链,借用构造函数优势在于可以在子类构造函数中向父类构造函数传递参数。
function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this,"Andy"); //实际上是给SubType添加了一个name属性
this.age = 20;
}
var instance = new SubTyoe();
alert(instance.name); //"Andy"
alert(instance.age); //20
3.2.2 借用构造函数的缺点
缺点和构造函数模式是相似的。
3.3 组合继承
将上述两种方法合二为一。
- 原型链实现实现对原型属性和方法的继承,实现函数复用
- 借用构造函数来实现对实例属性的继承,保证每个实例有自己的属性
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}//父类的构造函数,父类有2个属性
SuperType.prototype.sayName = function(){
alert(this.name);
};//父类原型方法
function SubType(name,age){
//继承属性
SuperType.call(this, name);
this.age = age; //子类特有的属性
}//子类的构造函数
SubType.prototype = new SuperType();//继承方法
SubType.prototype.sayAge = function(){
alert(this.age);
};//子类添加的方法
var instance1 = new SubType("Andy",20);
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"
instance1.sayName(); //"Andy"
instance1.sayAge(); //20
var instance2 = new SubType("Bob",2);
alert(instance2.colors);//"red,blue,green"
instance2.sayName(); //"Bob"
instance2.sayAge(); //2
组合继承避免了原型链和借用构造函数模式的缺点,是JS中最常用的集成模式。
3.4 原型式继承
这种方法没有严格意义上的构造函数,其想法是借助原型可以基于已有对象创建新对象,并且不必因此创建自定义类型。
function object(o){
function P(){} //在函数内部先创建一个临时的构造函数,
P.prototype = o; //将传入的对象作为这个构造函数的原型
return new P(); //返回这个临时类型的一个新实例
}
//本质是对传入参数的一次浅复制
JS通过Object.create()
方法规范化了原型式继承。
这种方法在于方便,但同样存在原型模式的缺点。
3.5 寄生式继承
这种方法与工厂模式类似,创建一个仅用于封装继承过程的函数。
function createAnother(){
var clone = object(original); //通过调用函数创建新对象
clone.sayHi = function(){ //给对象增加方法
alert("hi");
};
return clone; //返回该对象
}
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
同样存在多个实例多次定义函数的情况,执行效率会降低。
3.6 寄生组合式继承
组合继承有自己的缺点,无论什么情况下,都会调用两次父类的构造函数,创建子类原型的时候和子类构造函数内部。
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}//父类的构造函数
SuperType.prototype.sayName = function(){
alert(this.name);
};//父类原型方法
function SubType(name,age){
SuperType.call(this, name); //第二次调用父类构造函数 在新对象上创建了实例属性name,colors
this.age = age;
}//子类的构造函数
SubType.prototype = new SuperType(); //第一次调用父类构造函数 SubType得到name和colors属性
SubType.prototyp.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};//子类添加的方法
寄生组合式继承
- 通过借用构造函数继承属性
- 通过原型链的混成形式来继承方法
本质就是使用寄生式继承来继承父类的原型,再将结果指定给子类型的原型
基本模式如下:
function inheritPrototype(subType,superType){
var prototype = object(superType.prototype); //创建对象 创建父类原型的一个副本
prototype.constructor = subType; //增强对象 为创建的副本添加constructor属性 弥补因重写原型而失去的默认的constructor属性
subType.prototype = prototype; //指定对象 将新创建的对象赋值给子类型的原型
}
使用寄生组合式继承后,原来的例子可以修改为如下形式:
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}//父类的构造函数
SuperType.prototype.sayName = function(){
alert(this.name);
};//父类原型方法
function SubType(name,age){
SuperType.call(this, name); //第二次调用父类构造函数 在新对象上创建了实例属性name,colors
this.age = age;
}//子类的构造函数
inheritPrototype(subType,superType);
SubType.prototype.sayAge = function(){
alert(this.age);
};//子类添加的方法
这种继承方式只调用了一次父类构造函数,避免在子类原型上创建不必要的属性,原型链也保持了原样,可以用instanceof和isPrototypeOf()判别类型,是引用类型最理想的继承范式。