一、原型链继承
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
寄生组合式继承比组合式继承效率高,因为寄生组合式继承只调用了一次父类的构造函数,并且也因此避免了在子类原型上创建不必要的、多余的属性,与此同时原型链还能保持不变。因此寄生组合式继承是引用类型最理想的继承范式。
来源:https://www.cnblogs.com/jiafifteen/p/12201328.html