原型模式
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而则个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
换句话说,不必再构造函数中定义对象实例的信息,而是可以将这个写信息直接添加到原型对象中。
示例:
function Person(){
}
Person.prototype.name = "Nichols";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName();
var person2 = new Person();
person2.sayName();
alert(person1.sayName == person2.sayName) //true
理解原型对象
无论什么时候,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。
下图展示了各个对象之间的关系:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先
从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,
则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这
个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜
索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再
问:“ person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函
数。当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。而这正是多个
对象实例共享原型所保存的属性和方法的基本原理。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。(只能屏蔽,但不能重写)
换句话说,给实例添加一个属性只会阻止我们访问原型中的那个属性,但不会修改原型中的属性。即使设置这个属性为null,额只会在实例中设置这个属性,而不会恢复其指向原型的链接。
不过,使用delete操作符则可以完全删除实例属性。如下例所示:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.job = "Software Engineer";
Person.sayName = function(){
alert(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Gray";
alert(person1.name); //gray
alert(person2.name); //Nicholas
delete person1.name;
alert(person1.name); //Nicholas
原型与in操作符
in操作符有两种使用方式:
- 单独使用
- 在for-in 循环中使用
在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论属性存在于实例中还是原型中。
看下面的例子:
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——来自原型
alert(person2.hasOwnProperty("name")); //false
alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ——来自原型
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
由于in操作时只要通过对象能够访问到属性就返回true,hasOwnProperty()只在属性存在于实例中才返回true,因此只要in操作符返回true而hasOwnProperty()返回false, 就可以确定属性时原型中的属性。
更简单的原型语法
function Person(){
}
Person.prototype = {
name: "Nicholas";
age: 29;
job: "Software Engineer";
sayName: function(){
alert(this.name);
}
}
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。看下面的例子。
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实
例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,
但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参
数;可谓是集两种模式之长。下面的代码重写了前面的例子。
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.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
动态原型模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
//method
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(o.name);
};
return o;
}
var friend = new Person("Nicholas",29,"Software Engineer");
friend.sayName();
稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在
一些安全的环境中(这些环境中会禁止使用 this 和 new ),或者在防止数据被其他应用程序(如 Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this ;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的 Person 构造函数重写如下。
function Person(name,age,job){
var o = new Object();
o.sayName = function(){
alert(name);
};
return o;
}
稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用。
继承
原型链
基本思想:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如,我们让原型对象等于另一个类型的实例,显然,此时的原型对象将包含一个指向另一个原型的指针。
实现原型链有一种基本模式,其代码大致如下:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType(){
this.subProperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subProperty;
}
var instance = new SubType();
console.log(instance.getSuperValue());//true
console.log(instance.getSubValue());//false
这个例子中的实例以及构造函数和原型之间的关系如图
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系
- instanceof操作符
只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true
alert(instance instanceof Object); //true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true
只要是原型链中出现过的原型
- isPrototypeOf()方法
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例原型,因此isPrototypeOf()方法也会返回true
谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
原型链的问题
通过原型来实现继承的时候,原型实际上会变成另一个类型的实例。于是,原先的实例属性变成了现在原型里的属性了。
在创建子类的实例时,不能向超类型的构造函数中传递参数。
借用构造函数(伪造对象/经典继承)
基本思想:
在子类型的构造函数内部调用超类型的构造函数。这相当于在子类型的构造函数内部复制了一个超类型的副本
优势
可以在子类型的构造函数中向超类型构造函数传递参数。
劣势
在超类型的原型中定义的方法,对子类型是不可见的。
组合继承(伪经典继承)
指的是将原型链和借用构造函数的技术组合到一块
基本思路:
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
看下面的例子:
function SuperType(name){
this.colors = ["red","blue"];
this.name = name;
}
SuperType.prototype.sayName = function () {
console.log(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 () {
console.log(this.age);
}
运行结果:
[ ‘red’, ‘blue’ ];
如果我们去掉
SuperType.call(this,name);
这一行
运行结果:
[ ‘red’, ‘blue’, ‘white’ ]
为什么会这样呢?因为SuperTye.call(this,name)相当于复制了SuperType的属性然后添加到了SubType的构造函数中。
当构造函数和函数原型中同时拥有两个相同的属性,解析器会优先读取构造函数中的属性。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的有点。成为了JavaScript中最常用的继承模式。
原型式继承
看下面的函数:
function object(o){
function F(){};
F.prototype = o;
return new F();
}
上面这个函数将传入的对象作为了这个构造函数的原型,并且最后返回了这个临时类型的一个实例。
实际上object 完成了对传入对象o的一次浅复制
看下面的代码:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
这种原型式继承,必须有一个对象可以作为另一个对象的基础。这个新对象将person作为原型,所以它的原型中就包含一个基本类型值的属性和一个引用类型值的属性。
所以person.friends会被拥有相同原型的实例所共有。
ECMAScript5通过新增Object.create()方法规范了原型式继承,这个方法接受两个参数:
- 一个用作新对象原型的对象
- 一个为新对象定义额外属性的对象
在传入一个参数的情况下。Object.create()与object()方法的行为相同。
寄生式继承
寄生式继承的思路与寄生构造函数类似,即创建一个仅用于封装继承过程的函数。在内部以某种方式来增强对象,再返回对象
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log("Hi!");
};
return clone;
}
寄生组合式继承
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数。并且子类型最终会包含超类型对象的全部实例属性,我们不得不再调用子类型构造函数时重写这些属性。
基本思路:
我们所需要的无非就是超类型的原型的一个副本,然后在赋值给新对象这个值,让其拥有超类型原型的属性,而不必继承超类型的构造函数中的属性。
寄生组合式继承的基本模式如下:
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//指定对象
}
对比:
使用组合继承:
function SuperType(name){
this.name = name;
this.colors = ["red","blue"];
}
SuperType.prototype.sayName = function(){
console.log(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(){
console.log(this.age);
}
上例中,两次调用SuperType的构造函数,并且子类完全继承了超类的构造函数中的属性。
使用寄生组合式继承:
function inheritPrototype(subType,superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = ["red","blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
使用寄生组合式继承则可以对是否要继承超类构造函数中的属性做出选择,如果要继承超类构造函数中的属性,则需要在子类的构造函数中调用超类构造函数。如不需要继承超类的构造函数中的属性,则不要在子类的构造函数中调用超类的构造函数。并且,由于只调用一个超类的构造函数,寄生组合式继承的效率高于组合式继承。
常见问题:
Q1: 如何避免原型中引用类型的值会被同个原型的其他实例修改?
A1: 在实例中创建和原型中引用类型同名的值,实例中的属性会覆盖掉原型的属性。
Q2: 如何避免原型链中父类的实例属性中的引用类型被子类继承变为原型属性后引起的引用类型值被其他实例修改的问题?
A2: 采用组合继承的方式,在子类的构造函数当中调用父类的构造函数。
Q3: prototype 与 proto 属性有什么区别?
A3: prototype是存在于构造函数中的属性。 proto 是存在于实例中的属性