一、原型链继承
1.1 实现
基本思想:重写子类的原型对象,让原型对象等于父类的实例
function Animal(color) {
this.kind = ['cat', 'dog'];
this.color = color;
}
Animal.prototype.getKind = function () {
return this.kind;
}
function Cat(name) {
this.name = name;
}
// 让Cat.prototype等于Animal的实例
// 传入超类Animal的构造函数中的参数,被Cat的所有实例共享
Cat.prototype = new Animal('black');
Cat.prototype.getColor = function () {
return this.color;
}
const cat1 = new Cat('wangcai');
let kind1 = cat1.getKind();
console.log(kind1); // [ 'cat', 'dog' ]
console.log(cat1.getColor()); // black
const cat2 = new Cat('xiaohei');
let kind2 = cat2.getKind();
console.log(kind2); // [ 'cat', 'dog' ]
console.log(cat2.getColor()); // black
注意:
- Cat.prototype.constructor指向Animal,因为 Cat.prototype = new Animal('black') 重写了Cat.prototype为Animal的一个实例,实例对象中没有constructor属性,因此当访问Cat.prototype.constructor时,实际上访问的是其父类型原型上的constructor
console.log(Cat.prototype.constructor === Animal); // true
console.log(Cat.prototype.constructor === Cat); // false
- 使用原型链继承时,如果重写原型链之前在原型链上添加了属性或者方法,重写原型链后将无法访问到刚刚添加的属性或方法
// 先在原型上添加了方法
Cat.prototype.getColor = function () {
return this.color;
}
// 然后重新原型对象
Cat.prototype = new Animal('black');
const cat1 = new Cat('wangcai');
// 调用原型上的方法报错
cat1.getColor(); // TypeError: cat1.getColor is not a function
1.2 判断原型与实例之间的关系
- instanceof:只要是派生出该实例的原型链中出现过的构造函数,都会返回true
// 使用instanceof操作符判断原型与实例之间的关系:
// 只要是派生出该实例的原型链中出现过的构造函数,都会返回true
console.log(cat1 instanceof Cat); // true
console.log(cat1 instanceof Animal); // true
console.log(cat1 instanceof Object); // true
- isPrototypeOf:只要是派生出该实例的原型链中出现过的原型,都会返回true
// 使用isPrototypeOf方法判断原型与实例中的关系:
// 只要是派生出该实例的原型链中出现过的原型,都会返回true
console.log(Cat.prototype.isPrototypeOf(cat2));
console.log(Animal.prototype.isPrototypeOf(cat2));
console.log(Object.prototype.isPrototypeOf(cat2));
1.3 缺点
- 父类的实例属性会被子类的所有实例共享,当通过子类型修改父类的实例属性(值为引用类型)时,会影响到子类型的其他实例
// 通过cat1修改kind属性的值会影响到Cat的其他实例
kind1.push('pig');
console.log(kind1); // [ 'cat', 'dog', 'pig' ]
console.log(kind2); // [ 'cat', 'dog', 'pig' ]
// 在cat1上添加了一个同名属性kind,不会影响到其他实例
cat1.kind = 'cat';
console.log(cat1.getKind()); // cat
console.log(cat2.getKind()); // [ 'cat', 'dog', 'pig' ]
分析:kind是Animal类型的实例属性,当让Cat.prototype等于Animal类型的实例时,kind会变成Cat.prototype中的一个属性,Cat类型的所有实例都会共享Cat.prototype上的kind属性,因此Cat类型的某个实例如果修改(是修改,不是添加)了kind属性的值,会影响到Cat类型其他实例的这个属性。
二、借用构造函数继承
2.1 实现
基本思想:在子类型的构造函数内部调用父类型的构造函数
借用构造函数继承解决了原型链继承的带来的一些问题:
function Animal(color) {
this.kind = ['cat', 'dog'];
this.color = color;
}
Animal.prototype.getKind = function () {
return this.kind;
}
function Cat(name, color) {
this.name = name;
// 调用父类构造函数,实现继承
Animal.call(this, color)
}
Cat.prototype.getColor = function () {
return this.color;
}
const cat1 = new Cat('wangcai', 'black');
const cat2 = new Cat('xiaohei', 'white');
// 修改kind属性,不会影响到其他实例
cat1.kind.push('pig');
console.log(cat1.kind); // [ 'cat', 'dog', 'pig' ]
console.log(cat2.kind); // [ 'cat', 'dog' ]
2.2 缺点
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
console.log(cat1.getKind()); // TypeError: cat1.getKind is not a function
- 无法实现复用,子类型的每个实例都包含父类函数的副本,影响性能
三、组合继承(最常用的)
3.1 实现
基本思想:通过原型链实现对父类的原型属性和方法的继承,通过借用构造函数实现对父类的实例属性和方法的继承
function Animal(color) {
this.kind = ['cat', 'dog'];
this.color = color;
}
Animal.prototype.getKind = function () {
return this.kind;
}
function Cat(name, color) {
this.name = name;
// 调用父类构造函数,实现继承(第二次调用)
Animal.call(this, color)
}
// 第一次调用Animal(),使Cat.prototype为Animal类型的实例对象
Cat.prototype = new Animal();
// 在Cat.prototype上添加constructor属性,屏蔽了其原型对象Animal.prototype上的constructor属性,能更准确的判断出Cat实例的类型
Cat.prototype.constructor = Cat;
Cat.prototype.getColor = function () {
return this.color;
}
const cat1 = new Cat('wangcai', 'black');
const cat2 = new Cat('xiaohei', 'white');
// 修改kind属性,不会影响到其他实例
cat1.kind.push('pig');
console.log(cat1.kind); // [ 'cat', 'dog', 'pig' ]
console.log(cat2.kind); // [ 'cat', 'dog' ]
// 可以访问父类原型上的方法
console.log(cat1.getKind()); // [ 'cat', 'dog', 'pig' ]
console.log(cat2.getKind()); // [ 'cat', 'dog' ]
// 子类的实例可以直接通过constructor属性判断其类型
console.log(cat1.constructor === Cat); // true
console.log(cat2.constructor === Cat); // true
分析:
- 第一次调用Animal()时,在Cat.prototype上添加了kind、color属性;
- 第二次调用Animal()时,在Cat实例上添加了kind、color属性;
- 实例对象cat1/cat2上的kind、color属性屏蔽了原型上的kind、color属性。
3.2 缺点
- 使用子类创建实例时,实例与原型对象上会有一组同名属性或方法
四、原型式继承
4.1 实现
基本思想:借助原型可以基于已有对象创建新对象,同时还不必创建自定义类型。
function object(o) {
// 创建一个临时性的构造函数F
function F() {}
// 将传入的对象作为这个F类型的原型
F.prototype = o;
// 返回一个F类型的实例
return new F();
}
const person = {
name: 'Lily',
interests: ['music', 'book']
}
const person1 = object(person);
const person2 = object(person);
// person1、person2的类型与person一样为Object,不需要创建一个新的自定义类型
console.log(person1.constructor); // [Function: Object]
console.log(person2.constructor); // [Function: Object]
注意:
- object方法对传入的对象进行了一次浅复制,将构造函数F的原型指向传入的对象,因此实例对象与传入的对象共享属性。
console.log(person1.interests); // [ 'music', 'book' ]
console.log(person2.interests); // [ 'music', 'book' ]
// 通过一个实例对象修改某个引用类型的属性的值后,会影响其他实例,以及传入的基本对象
person1.interests.push('play');
console.log(person1.interests); // [ 'music', 'book', 'play' ]
console.log(person2.interests); // [ 'music', 'book', 'play' ]
console.log(person.interests); // [ 'music', 'book', 'play' ]
- ES5中的Object.create()方法规范了原型式继承。这个方法接收两个参数,第一个参数表示的是用作新对象原型的对象,第二个参数是一个为新对象定义额外属性的对象,这个对象中的每个属性都是通过自己的描述符定义的。用这种方法定义的属性会覆盖原型对象上的同名属性。
let person = {
name: 'Lily',
interests: ['music', 'book'],
friends: ['Lucy', 'Jack']
}
let newPerson = Object.create(person, {
name: {
value: 'Lucy'
},
age: {
value: 18
},
friends: {
value: ['Lily', 'Jack']
}
})
// 属性都在原型对象上
console.log(newPerson); // {}
// 覆盖了person上的name属性值
console.log(newPerson.name); // Lucy
console.log(newPerson.age); // 18
console.log(newPerson.interests); // [ 'music', 'book' ]
// 与person共享interests属性
newPerson.interests.push('sing');
console.log(newPerson.interests); // [ 'music', 'book', 'sing' ]
console.log(person.interests); // [ 'music', 'book', 'sing' ]
// 覆盖了person上的frineds,修改frineds不会影响person上的同名属性
console.log(newPerson.friends); // [ 'Lily', 'Jack' ]
newPerson.friends.push('Mary');
console.log(newPerson.friends); // [ 'Lily', 'Jack', 'Mary' ]
console.log(person.friends); // [ 'Lucy', 'Jack' ]
Object.create()方法的实现原理:
// 模拟create的实现
function object(o, attr) {
function F() {}
F.prototype = o;
let f = new F();
if (attr && typeof attr === 'object') {
Object.defineProperties(f, attr)
}
return f;
}
const newPerson = object(person, {
name: {
value: 'Lucy'
},
age: {
value: 18
},
friends: {
value: ['Lily', 'Jack']
}
})
// 属性都在原型对象上
console.log(newPerson); // {}
// 覆盖了person上的name属性值
console.log(newPerson.name); // Lucy
console.log(newPerson.age); // 18
console.log(newPerson.interests); // [ 'music', 'book' ]
// 与person共享interests属性
newPerson.interests.push('sing');
console.log(newPerson.interests); // [ 'music', 'book', 'sing' ]
console.log(person.interests); // [ 'music', 'book', 'sing' ]
// 覆盖了person上的frineds,修改frineds不会影响person上的同名属性
console.log(newPerson.friends); // [ 'Lily', 'Jack' ]
newPerson.friends.push('Mary');
console.log(newPerson.friends); // [ 'Lily', 'Jack', 'Mary' ]
console.log(person.friends); // [ 'Lucy', 'Jack' ]
4.2 缺点
- 当基本对象属性的值为引用类型时,会被所有实例对象共享,就跟使用原型模式一样。
4.3 使用场景
当仅仅想让一个对象与另一个对象保存类似,而不想创建新的类型时,可以使用该模式
五、寄生式继承
5.1 实现原理:寄生式继承与原型式继承的思路很相似,区别在于寄生式继承增强了对象
function object(o) {
function F() {}
F.prototype = o;
return new F()
}
function createObject(original) {
// 调用object创建一个新对象
let clone = object(original);
// 为新对象添加方法
clone.sayName = function () {
console.log(this.name)
}
return clone;
}
let person = {
name: 'Lily',
interests: ['music', 'book'],
friends: ['Lucy', 'Jack']
}
let otherPerson = createObject(person);
otherPerson.sayName(); // Lily
// 每个实例上的sayName方法指向不同的引用,虽然功能一样
let otherPerson2 = createObject(person);
console.log(otherPerson.sayName === otherPerson2.sayName);
5.2 缺点
- 与原型式继承一样,如果基本对象属性的值为引用类型,那么基于该对象创建出来的对象将共享这个属性;
- 使用组合式继承为对象添加函数时,无法实现函数复用,降低效率。
六、寄生组合式继承
6.1 基本思想:通过借用构造函数来继承父类的实例属性,通过寄生式继承来继承父类的原型属性和方法
// 原型式:将一个空对象的原型指向一个已有对象,最后返回这个空对象
function object(base) {
function F() {}
F.prototype = base;
return new F;
}
// 组合式:对原型式中的返回的对象进行增强
// 利用组合式继承父类的原型
function createObject(subType, superType) {
// 创建对象:返回父类原型的副本
let prototype = object(superType.prototype);
// 增强对象:指定子类类型
prototype.constructor = subType;
// 继承父类的原型
subType.prototype = prototype;
}
function Animal(kind) {
this.kind = kind;
}
Animal.prototype.getKind = function () {
console.log(this.kind);
}
function Cat(name) {
this.name = name;
// 借用构造函数:继承父类的实例属性
Animal.call(this, 'cat');
}
// 继承父类的原型
createObject(Cat, Animal);
Cat.prototype.getName = function () {
console.log(this.name);
}
const cat1 = new Cat('xiaohei');
cat1.getName(); // xiaohei
cat1.getKind(); // cat
console.log(cat1 instanceof Cat); // true
console.log(Cat.prototype.isPrototypeOf(cat1)); // true
console.log(cat1 instanceof Animal); // true
console.log(Animal.prototype.isPrototypeOf(cat1)); // true
寄生组合式继承比组合式继承效率高,因为寄生组合式继承只调用了一次父类的构造函数,并且也因此避免了在子类原型上创建不必要的、多余的属性,与此同时原型链还能保持不变。因此寄生组合式继承是引用类型最理想的继承范式。