JS继承细说

157 阅读5分钟

JS的继承

一、原型链

通过原型链的方式来实现继承

关键点:子类的原型指向父类的实例(本质就是重写了原型对象)

优点

  • 简单,易实现
  • 父类新增原型方法/原型属性,子类都能访问

缺点

  • 无法实现多继承
  • 引用类型的值会被实例共享
  • 子类型无法给超类型传递参数
function Parent() {
  this.parentName = '父类';
}
Parent.prototype.getParentName = function() {
  return this.parentName;
};

function Child() {
  this.childName = '子类';
}
Child.prototype = new Parent();	// 关键点:子类的原型指向了父类的实例
Child.prototype.getChildName = function() { // 子类要在继承后定义新方法,否则会被覆盖
  return this.childName
};

var c = new Child();
console.log(c.getParentName()); // '父类'

二、借用构造函数

借用构造函数即通过在子类型的函数中调用超类型的构造函数来实现,

这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。

关键点在子类型构造函数的内部调用超类型构造函数

优点

  • 解决了引用类型的值被实例共享的问题
  • 可以向超类传递参数
  • 可以实现多继承(call 若干个超类)

缺点

  • 不能继承和访问超类原型上的属性和方法
  • 无法实现函数复用,由于 call 有多个父类实例的副本,性能损耗。
  • 原型链丢失
function Parent(name) {
  this.name = name;
  this.hobbies = ["sing", "dance", "rap"];
}

function Child(name) {
  Parent.call(this, name); // 调用父类构造函数
  this.age = 24
}

var c1 = new Child('c1');
var c2 = new Child('c2');
c1.hobbies.push('coding');

console.log(c1.hobbies)
console.log(c2.hobbies)
console.log(c1 instanceof Parent)
console.log(c1 instanceof Child)

三、组合继承

组合继承:组合继承是将原型链和借用构造函数组合起来使用的一种方式。

通过借用构造函数的方式来实现类型的属性的继承

通过将子类型的原型设置为超类型的实例来实现方法的继承

这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

关键点:组合原型链继承和借用构造函数继承

优点

  • 弥补了上面两者的问题

缺点:

  • 调用了两次超类型构造函数,子类型的原型中多了很多不必要的属性。
function Parent(name){
  this.name = name;
  this.hobbies = ["sing", "dance", "rap"];
}
Parent.prototype.getName = function(){
  return this.name
}
function Child(name){
  Parent.call(this, name);
  this.age = 24
}

Child.prototype = new Parent('父类')
var c1 = new Child('c1');
var c2 = new Child('c2');

console.log(c1.hasOwnProperty('name')); // true
console.log(c1.getName()); // "c1"

c1.hobbies.push('coding');
console.log(c1.hobbies); // ["sing", "dance", "rap", "coding"]
console.log(c2.hobbies); // ["sing", "dance", "rap"]

四、原型式

原型式继承

主要思路:基于已有的对象来创建新的对象

实现原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。

这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

关键点:基于当前已有对象创建新对象。(Object.create)

缺点:

Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能

// 用法一:创建一个纯洁的对象:对象什么属性都没有
Object.create(null);

// 用法二:创建一个子对象,它继承自某个父对象
var o1 = {
  name: '父对象',
  say: function() {}
}
var o2 = Object.create(o1); // 原型式继承

五、寄生式继承

寄生式继承

主要思路:创建一个用于封装继承过程的函数通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象

这个扩展的过程就可以理解是一种继承。

关键点:结合原型式继承和工厂模式,创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象

本质上和原型式继承差不多

优点

  • 对一个简单对象,如果不是我们的自定义类型时,可以较好实现继承

缺点

  • 没有办法实现函数的复用。
function createAnother(origin) {
  var clone = Object.create(origin); // 通过调用函数创建一个新对象
  clone.sayHi = function() { // 以某种方式来增强这个对象
    alert("Hi");
  };
  return clone; // 返回这个对象
}

var o1 = {
  name: "父对象",
  hobbies: ["sing", "dance", "rap"]
};
var o2 = createAnother(o1);
o2.sayHi();

六、寄生组合式

寄生式组合继承

寄生组合式继承:借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式

缺点 使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。

寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

关键点:目前最优解

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.create(SuperType.prototype); // 将子类的原型指向超类的原型
  SubType.prototype.constructor = SubType;
}

// 实例
function Parent(name) {
  this.name = name;
  this.hobbies = ["sing", "dance", "rap"];
}
Parent.prototype.getHobbies = function(){
  return this.hobbies
}
function Child(name) {
  Parent.call(this, name);
  this.age = 24
}

inheritPrototype(Child, Parent)

// 测试结果
var c1 = new Child('c1');
var c2 = new Child('c2');

console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true
console.log(c1.constructor); // Child
console.log(Child.prototype.__proto__ === Parent.prototype); // true
console.log(Parent.prototype.__proto__ === Object.prototype); // true

c1.hobbies.push('coding');
console.log(c1.getHobbies()); // ["sing", "dance", "rap", "coding"]
console.log(c2.getHobbies()); // ["sing", "dance", "rap"]