6.2.1、对象字面量创建单个对象
var obj = {name:"Mandy",age:"20"};
缺点:使用同一个接口创建很多对象,会产生大量重复代码
6.2.2、工厂模式——用函数来封装以特定接口创建对象的细节
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("Tony","21","teacher");
var person2 = createPerson("Mandy","20","doctor");
函数createPerson() 能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次的调用这个函数,而每次他都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(怎么知道一个对象的类型)
6.2.3、构造函数模式
构造函数可以创建特定类型的对象,向Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。也可以创建自定义构造函数,从而定义自定义对象类型的属性和方法。例如,可以适用构造函数模将前面例子重写如下:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("Tony","21","teacher");
var person2 = new Person("Mandy","20","doctor");
Person() 和 createPerson()的不同之处:
创建Person的新实例,必须使用new操作符,这种方式调用构造函数实际上会经历以下4个步骤:
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这正式构造函数模式胜过工厂模式的地方
构造函数虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。person1和person2都有一个sayName()方法,但它们是不同实例的方法。上面的构造函数也可以这样写:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");//与声明函数逻辑上是等价的
}
创建两个完全同样任务的Function实例确实没有必要,况且有this对象在,根本不用在执行代码前就把函数绑定到特定的对象上面,因此可以把函数定义转移到构造函数外部来解决这个问题
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Tony","21","teacher");
var person2 = new Person("Mandy","20","doctor");
这种方法把函数定义在了全局函数,如果要定义多个方法就会出现很对个全局函数,这些问题可以用原型模式来解决。
6.2.4、原型对象的问题
原型对象也不是没有缺点,对于包含引用类型值的属性来说,问题就很突出,看下面的例子
funciton Person(){
}
Person.prototype = {
construction : Person,
name : "Tony",
age : 21,
job : "teacher",
friends : ["Shelby","Court"],
sayName ; function(){
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends);// ["Shelby","Court","Van"]
alert(person2.friends);// ["Shelby","Court","Van"]
alert(person1.friends === person2.friends);// true
这里在person1中修改friends,在person2中也会反映出来。可是,实例一般都是要有属于自己的全部的属性。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
6.2.5、组合使用构造函数模式和原型模式
创建自定义类型的最常见方法,就是组合使用构造函数模式与原型模式,构造函数模式用于自定义实例属性,而原型模式用于定义方法和共享的属性。这样,每个实例都会有自己的一份实例属性的副本,但有同时共享着对方法的引用,最大限度的节省了内存。另外,这种混成模式还支持向构造函数传递参数:可谓是两种模式之长:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Tony","21","teacher");
var person2 = new Person("Mandy","20","doctor");
person1.friends.push("Van");
alert(person1.friends);// ["Shelby","Court","Van"];
alert(person2.friends);// ["Shelby","Court"];
alert(person1.friends === person2.friends);// false
alert(person1.sayName=== person2.sayName);// true
这种构造函数与原型混成的模式,是目前ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来自定义引用类型的一种默认模式。
6.2.6、动态原型模式
动态原型是把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,来看个例子:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
//方法
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person("Tony",21,"teacher");
friend.sayName();
sayName()方法不存在的情况下,才会将他添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了,不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映,因此,这种方法确实可以说非常完美。其中,if语句检查的可以是初始化之后应该存在的任何属性和方法——不必用一大堆if语句检查每个属性和方法,只要检查其中一个即可。对于才用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。
注意:使用动态原型模式时,不能使用对象字面量重写原型,如果在已经创建了实例的情况下重写原型,就会切断现有实例和原型之间的联系
许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际方法。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
6.3.1、原型链
ECMAScript 将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指针指向构造函数,而实例都包含一个指向原型对象的内部指针。
实现原型链有一种基本模式,代码大致如下:
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();
aler(instance.getSuperType());//true
1、谨慎地定义方法
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法,但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后,看下面的例子:
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;
}
//重写超类型中的方法
SubType.prototype.getSuperValue = function(){
return false;
}
var instance = new SubType();
aler(instance.getSuperType());//false
注意:通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样会重写原型链,如下:
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;
},
someOtherMethod : function(){
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue());//error
以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成了一个对象字面量而导致的问题。由于现在的原型包含的是一个Object实例,而非SuperType的实例,因此我们设想中的原型链已经被切断——SubType和SuperType之间已经没有关系了。
2、原型链的问题
原型链虽然很强大,可以用它来实现继承,但他也存在一些问题。其中,最主要的问题是来自包含引用类型值的原型,包含引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了。
第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。
6.3.2、借用构造函数
在解决原型中包含引用类型值所带来的问题的过程中,开发人员使用一种叫做借用构造函数的技术(有时也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创捷的对象上执行构造函数,如下:
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
通过call()方法“借调”了超类型的构造函数。我们实际上是在新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式,考虑到这些问题,借用构造函数也是很少单独使用的。
6.3.3、组合继承
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合在一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
aler(this.name);
}
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new SubType("Tony",21);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Tony"
instance1.sayAge(); // 21
var instance2 = new SubType("Mandy",20);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Mandy"
instance2.sayAge(); // 20
两个实例既分别拥有自己的熟悉——包括colors属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为JavaScript中最常用的继承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合函数创建的对象。