其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。
function Human(){
this.type = 'mammal'
}
Human.prototype.getType = function(){
return this.type;
};
function Person(){
this.alive = true;
this.foods = 'vegetable'
};
// 继承了Human
Person.prototype = new Human()
// 给原型添加方法的代码放在替换原型的语句之后
Person.prototype.getAlive = function(){
return this.alive;
}
var instance = new Person();
instance.getType(); // 'mammal'
console.log(instance.type); // 'mammal'
instance.getAlive(); // true
console.log(instance.foods); // vegetable
上述代码实现的本质是用一个新类型的实例重写原型对象。
原来存在于Human实例中的所有属性和方法,现在也存在于Person.prototype中。我们没有使用Person默认提供的原型,而是给它换了一个新原型;这个新原型就是Human的实例。
最终结果就是instance指向Person的原型,Person的原型又指向Human的原型。getType()方法仍然还在Human.prototype中,但type则位于Person.prototype中。这是因为type是一个实例属性,而getType()则是一个原型方法。既然Person.prototype现在是Human的实例,那么type当然就位于该实例中了。也就是说alive、foods存在于instance实例中,type存在于Person.prototype实例中。因为彼此都是互相的实例属性。
现在instance.constructor现在指向的是Human,这是因为原来Person.prototype被重写了的缘故。(实际上不是Person原型的constructor属性被重写了,而是Person的原型指向了另一个对象Human的原型,而这个原型对象的constructor属性指向的是Human)
通过实现原型链,本质上扩展了原型搜索机制。即如果搜索一个实例属性时,先搜索实例,继续搜索实例的原型,在通过原型链实现继承的情况下,搜索过程就可以沿着原型链继续向上。调用instance.getType()会经历三个搜索步骤:(1)搜索instance实例;(2)搜索Person.prototype;(3)搜索Human.prototype 搜索过程总是要一环一环地前行到原型链末端才会停下来。
如果上述代码,Person原型上若重新定义getType()方法。那么调用instance.getType()则会读取Person原型上的这个方法,屏蔽掉Human原型上的这个方法。但是Human的其他实例调用getType()方法则不受影响,继续调用的是Human原型上的该方法。
function Human(){
this.type = 'mammal'
}
Human.prototype.getType = function(){
return this.type;
};
function Person(){
this.alive = true;
this.foods = 'vegetable'
};
// 继承了Human
Person.prototype = new Human()
// 给原型添加方法的代码放在替换原型的语句之后
Person.prototype.getAlive = function(){
return this.alive;
};
Person.prototype.getType = function(){
return false;
};
const instance = new Person();
instance.getType(); // false
const people = new Human();
people.getType(); // 'mammal'
通过原型链实现继承的时候,不能使用对象字面量创建原型方法。因为这样会重写原型链。
function Human(){
this.type = 'mammal'
}
Human.prototype.getType = function(){
return this.type;
};
function Person(){
this.alive = true;
this.foods = 'vegetable'
};
// 继承了Human
Person.prototype = new Human()
// 给原型添加方法的代码放在替换原型的语句之后
Person.prototype = {
getAlive: function(){
return this.alive;
},
getAnother: function(){
return false;
}
}
const instance = new Person();
instance.getType(); // error
以上代码,刚刚把Human的实例赋给Person原型,紧接着又将原型替换成一个对象字面量。所以现在Person的原型包含的是一个Object的实例,而不是Human的实例。原型链已经被切断。Person与Human之间已经没有关系了。
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。也就是说Person继承了Human,而Human继承了Object。当调用instance.toString()时,实际上调用的是保存在Object.prototype中的那个方法。
由于原型链的关系,instance是Object、Person、Human中任何一个类型的实例。
console.log(instance instanceof Object); // true
console.log(instance instanceof Person); // true
console.log(instance instanceof Human); // true
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(Person.prototype.isPrototypeOf(instance)); // true
console.log(Human.prototype.isPrototypeOf(instance)); // true
(1)尽管原型链很强大,可以用它实现继承,但它也存在一些问题。最主要的问题是包含引用类型值的原型。在通过原型实现继承时,原型会变成另一个类型的实例。于是,原先实例属性也就成为现在原型属性。如果原先实例属性是一个引用类型,那么原型的其他实例都会共享这个属性。
function Human(){
this.colors = ['white','black']
};
function Person(){
};
// Person继承了Human
Person.prototype = new Human();
const instance1 = new Person();
instance1.colors.push('yellow');
console.log(instance1.colors); // ['white','black', 'yellow']
const instance2 = new Person();
console.log(instance2.colors); // ['white','black', 'yellow']
如上代码,所有Person实例,都共享了colors的属性。
(2)创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上应该说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
相对于原型而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。
function Human(name){
this.name = name;
};
Human.prototype.sayName = function(){
alert(this.name);
}
function Person(){
// 继承了Human,同时还传递了参数
Human.call(this, 'lee');
// 实例属性
this.age = 29;
};
const instance = new Person();
console.log(instance.name); // 'lee'
console.log(instance.age); // 29
instance.sayName(); // error
为了确保Human里的属性不会重写Person中的实例属性,所以在调用超类构造函数后,再添加应该在子类中定义的属性。
(1)方法都在构造函数中定义,函数复用无从谈起。
(2)在超类型的原型中定义的方法,对子类型而言也时不可见的,结果所有类型都只能使用构造函数模式。
考虑到这些问题,借用构造函数的技术也时很少单独使用的。
有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function Human(name){
this.name = name;
this.colors = ['black','white'];
};
Human.prototype.sayName = function(){
alert(this.name);
}
function Person(name, age){
// 继承了Human,同时还传递了参数
Human.call(this, name); // 第二次调用超类型Human()
// 实例属性
this.age = age;
};
Person.prototype = new Human(); // 第一次调用超类型Human()
Person.prototype.constructor = Person;
Person.prototype.sayAge = function(){
alert(this.age)
}
const instance = new Person('lee', 29);
instance.colors.push('yellow');
console.log(instance.colors); // ['black','white','yellow']
instance.sayName(); // 'lee'
instance.sayAge(); // 29
const instance1 = new Person('Alice', 24);
console.log(instance1.colors); // ['black','white']
instance1.sayName(); // 'Alice'
instance1.sayAge(); // 24
无论什么情况下,都会调用2次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。 如上面例子,在第一次调用Human构造函数时,Person.prototype会得到2个属性,name和colors;它们都是Human的实例属性,只不过现在位于Person.prototype的原型中。当调用Person构造函数时,又会调用一次Human构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。也就是说有2组name和colors属性。一组在实例上,一组在原型中。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为javascript中最常用的继承模式。而且instanceof和isPrototypeOf也能够用于识别基于组合继承创建的对象。
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
从本质上讲,object()对传入其中的对象执行了一次浅复制。
const person = {
name: 'lee',
friends: ['Alice', 'Bob', 'Jack']
}
// 这里的object是上面创建的那个object的函数
const anotherPerson = object(person);
anotherPerson.name = 'Gred';
anotherPerson.friends.push('Linda');
const yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Coco';
yetAnotherPerson.friends.push('lucy');
console.log(person.friends); // ['Alice', 'Bob', 'Jack', 'Linda', 'lucy']
console.log(anotherPerson .friends); // ['Alice', 'Bob', 'Jack', 'Linda', 'lucy']
console.log(yetAnotherPerson.friends); // ['Alice', 'Bob', 'Jack', 'Linda', 'lucy']
上面例子,person中存在一个基本类型的属性和一个引用类型的属性。这意味着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。
ECMAScript5通过新增了object.create()方法规范了原型式继承。这个方法接收2个参数:一个用作新对象原型的对象(可选的)和一个为新对象定义额外属性的对象。在传入一个参数的情况下,object.create()和上面定义的object()方法的行为相同。
const person = {
name: 'lee',
friends: ['Alice', 'Bob', 'Jack']
}
// 这里的object是上面创建的那个object的函数
const anotherPerson = object.create(person, {
name: 'Gred'
});
console.log(anotherPerson.name); // 'Gred'
与使用原型链实现继承有相似的问题。也就是包含引用类型的属性,会共享相应的值。但是如果只想让一个对象与另一个对象保持类似的情况,没必要创建构造函数。原型式继承式完全可以胜任。
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。
function createAnother(origin){
const clone = object(origin); // 通过调用函数创建一个新对象
clone.sayHi = function(){ // 以某种方式增强对象
alert('Hi');
};
return clone; // 返回这个对象
}
const person = {
name: 'lee',
friends: ['Alice', 'Bob', 'Jack']
}
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'Hi'
anotherPerson.friends.push('Lee');
console.log(anotherPerson.friends); // ['Alice', 'Bob', 'Jack', 'Lee']
console.log(person.friends); // ['Alice', 'Bob', 'Jack', 'Lee']
与构造函数模式类似,会因为不能做到函数复用,而降低效率。上面例子object()函数不是必须的,任何能够返回新对象的函数都适用于此模式。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也式一种有用的模式。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其基本思路式:不必为了指定子类型的原型而调用超类型的构造函数,我们所需的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(person, human){
const prototype = object(human.prototype); // 创建对象副本
prototype.constructor = person; // 增强对象
person.prototype = prototype; // 指定对象
}
在函数内部,第一步式创建超类型原型的一个副本。第二步式为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。
function Human(name){
this.name = name;
this.colors = ['black','white'];
};
Human.prototype.sayName = function(){
alert(this.name);
}
function Person(name, age){
// 继承了Human,同时还传递了参数
Human.call(this, name);
// 实例属性
this.age = age;
};
inheritPrototype(Person, Human);
Person.prototype.sayAge = function(){
alert(this.age)
}
这个例子的高效体现在它只调用了一次Human构造函数,并且因此避免了在Person.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof和isPrototypeOf()。开发人员普遍认为寄生组合式继承式引用类型最理想的继承范式。