本文记录一下原生js创建对象的方式和继承的方式,内容主要来源 《JavaScript高级程序设计(第3版) 》第六章。
原型链
首先介绍一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象(prototype),原型对象(prototype)都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(constructor)。
new 的时候,只是把Person的原型(prototype) new 过来了,与其它的无关了,Person的prototype现在已经指向的一个全新的对象,与Person原来的原型已经没有任何关系。也就是说,prototype其实可以理解为已经被破坏了
实例化的对象没有prototype,已经实例化了,就只有constructor了。构造函数才有prototype。
每个构造函数都有一个默认的属性prototype,而这个prototype的constructor默认指向这个构造函数
在prototype不修改的情况下,一个构造函数Person.prototype和这个构造函数所有实例化出来的对象的constructor都是同一个,都是看Person.prototype 等号右边这个东西
创建对象方式
1、 工厂模式
// 工厂模式
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var person1 = createPerson('Tom', 29, 'teacher');
var person1 = createPerson('Jack', 27, 'dockor');
函数createPerson()
能够根据接受的参数来构建一个包含所有必要信息的Person
对象。
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
缺点: 因为在工厂函数内部是通过new Object()
创建的临时对象,最后返回了这个临时对象,所以通过这个工厂函数实例化的都是一种对象类型。
2、 构造函数模式
// 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person('Tom', 29, 'teacher');
var person1 = new Person('Jack', 27, 'dockor');
构造函数模式与工厂模式的不同之处:
- 没有显示的创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句
要创建Person
的新实例必须使用new
操作符,这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
person1 和 person2 分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true
缺点: 构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。
3、原型模式
function Person(){}
Person.prototype={
constructor:Person,
name:'Nicholas',
age:29,
job:'Software Engineer',
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
缺点: 首先,它省略了喂构造函数传递初始化参数这一环节,结果所有实例都在默认情况下都将取得相同的属性值。原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性没问题,对于包含引用类型值的属性来说,问题就比较突出了,一个实例修改,所有实例使用的时候都会修改,产生意想不到的效果
4、组合使用构造函数模式和原型模式 (常用)
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("Nicholas",29,"Software Engineer");
var person2=new Person("Greg",27,"Doctor");
person1.friends.push("Van");
alert(person1.firends); //"Shelby,Count,Van"
alert(person2.firends); //"Shelby,Count"
alert(person1.firends===person2.firends); //false
alert(person1.sayName===person2.sayName); //true
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性和副本,但同时又共享着对方法的应用,最大限度的节省了内存。这种构造函数与原型混成的模式是目前使用最广泛、认同度最高的一种创建自定义类型的方法
5、动态原型模式 (常用)
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('Nicholas', 29, 'Software Engineer');
friend.sayName();
这里只在sayName()方法不存在的情况下,才会将它添加到原型中,这段代码只会在初次调用构造函数时才会执行,if语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法,只要检查其中一个即可,采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型(此种模式不能使用对象字面量重写原型,方法只能这样加在原型上,如果重写原型了,就会切断现有实例与新原型之间的联系)
6、寄生构造函数模式
function Person(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 friend=new Person("Nicholas",29,"Spftware Engineer");
friend.sayName(); //"Nicholas"
这个模式可以在特殊的情况下用来为对象创建构造函数,假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数,因此可以使用这个模式
function SpecialArray(){
//创建数组
var values=new Array();
//添加值
values.push.apply(values,arguments);
//添加方法
values.toPipedString=function(){
return this.join("|");
};
//返回数组
return values;
}
var colors=new SpecialArray("red","blue","green");
alert(colors.toPipedString()); //"red|blue|green"
这个模式还有一点需要说明:首先返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部创建的对象么有什么不同,为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
7、稳妥构造函数模式
function Person(name,age,job){
//创建要返回的对象
var o=new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName()=function(){
alert(name);
};
//返回对象
return o;
}
var friend=Person("Nicholas",29,"Software Engineer");
friend.sayName(); //"Nicholas"
这种模式最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点不同:一是新建对象的实例方法不引用this;二是不适用new操作符调用构造函数。
继承方式
1、原型链:
function SuperType(){
this.property=true;
}
SuperType.prototype.getSuperValue=function(){
return this.property;
};
function SubType(){
this.subproperty=false;
}
//继承了SubperType
SubType.prototype=new SuperType();
SubType.prototype.getSubValue=function(){
return this.subproperty;
};
var instance=new SubType();
alert(instance.getSuperValue()); //true
上面代码定义了两个类型:SuperType和SubType,每个类型分别有一个属性和一个方法,它们主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说原来存在于SuperType的实例中的所有属性和方法,现在也存在与SubType.prototype中了,在确立了继承关系之后,我们给SubType.prototype添加了一个方法,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法
原型链最主要的问题来自包含引用类型值的原型,会影响所有实例,原型链的第二个问题是在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。所以实践中很少会单独使用原型链
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、组合继承(伪经典继承、原型链&对象冒充继承) (常用)
function SuperType(name){
this.name=name;
this.colors=['red','bule','green'];
}
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('Nicholas',29);
instance1.colors.push('black');
alert(instance1.colors); //'red,blue,green,black'
instance1.sayName(); //'Nicholas'
instance1.sayAge(); //29
var instance2=new SubType('Greg',27);
alert(instance2.colors); //'red,blue,green'
instance2.sayName(); //'Nicholas'
instance2.sayAge(); //29
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为javascript中最常用的继承模式,而且instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象
4、原型式继承
ES5 通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
var person = {
name: 'Nicholas',
friends: ['Shelby','Court','Van']
};
var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAtherPerson = Object.create(person);
yetAtherPerson.name = 'Linda';
yetAtherPerson.friends.push('Barbie');
console.log(person.friends); //'Shelby,Court,Van,Rob,Barbie'
Object.create()方法的第二个参数与Object。defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符合定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
var person = {
name: 'Nicholas',
friends: ['Shelby','Court','Van']
};
var anotherPerson = Object.create(person,{
name: {
vale: 'Greg'
}
});
console.log(anotherPerson.name); //'Greg'
由于是ES5的语法,所以低版本浏览器不支持直接使用的。
在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型模式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
5、寄生式继承
暂不多解释。
6、寄生组合式继承
function SuperType(name){
this.name=name;
this.colors=['red','bule','green'];
}
SuperType.prototype.sayName=function(){
alert(this.name);
};
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age=age;
}
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subType,superType){
var prototype=object(superType.prototype); //创建对象
prototype.constructor=subType; //增强对象
subType.prototype=prototype; //指定对象
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge=function(){
alert(this.age);
};
SubType.prototype=new SuperType();
var instance=new SubType("Nicholas",29);
这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保证不变;因此,还能够整正常使用instanceof和isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承方式。