JS的继承方法总结

153 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

JS的继承方法总结

一、原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

原型链继承实现的本质: 是改变构造函数的.prototype的指向。

原型链继承的不足: 不能通过子类构造函数向父类构造函数传递参数。

  function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
  }
  function Child1() {
    this.type = 'child2';
  }
  Child1.prototype = new Parent1();
  console.log(new Child1());
const s1 = new Child()
const s2 = new Child()
s1.play.push(4)
console.log(s1.play, s2.play)   // [1, 2, 3, 4]

缺点:由于实例使用的是同一个原型对象,因此内存空间是共享的,所以当一个实例发生变化的时候,另外一个也会随之发生变化。

二、构造函数继承

为了解决原型包含引用值导致的继承问题,就是在子类构造函数中调用父类构造函数。可以使用call()或apply()的方法。可以解决向父构造函数传参的问题,可以继承父类的实例属性和方法,但是不能获取父构造函数原型上的属性和方法。

function Parent(){
  this.name = "parent"
}

Parent.prototype.getName = function(){
  return this.name
}

function Child(){
  Parent.call(this)
  this.type = "Child"
}

let child = new Child()

console.log(child)  //没问题
console.log(child.getName())  //报错

缺点:只能继承父类的实例属性和方法,不能继承原型的属性和方法。

三、组合继承

综合了原型链构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。使用call或apply和原型继承。父类构造函数的属性和方法继承到了子类构造函数的实例中,并且继承了父类构造函数原型对象上的成员,但会给子类添加很多不必要的属性和方法。

  function Parent () {
    this.name = 'parent';
    this.play = [1, 2, 3, 5];
  }
 
  Parent.prototype.getName = function () {
    return this.name;
  }
  function Child() {
    // 第二次调用 Parent()
    Parent.call(this);
    this.type = 'child';
  }
 
  // 第一次调用 Parent()
  Child.prototype = new Parent();
  // 手动挂上构造器,指向自己的构造函数
  Child.prototype.constructor = Child;
  var s1 = new Child();
  var s2 = new Child();
  s1.play.push(4);
  console.log(s1.play, s2.play);  // 不互相影响
  console.log(s1.getName()); // 正常输出'parent'
  console.log(s2.getName()); // 正常输出'parent'

缺点:可以看到调用了两次父类构造函数,造成额外性能开销,耗内存。

四、寄生组合式继承

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

缺点:实现起来比较复杂

//核心:通过寄生方式,砍掉父类的实例属性,这样,在调用俩次父类的构造的时候,就不会初始化俩次实例方法/属性,避免了组合继承的缺点。
function Cat(name) {
	Animal.call(this);
	this.name = name || 'Tom';
}
(function() {
	var Super = function() {};  //创建一个没有实例的方法类。
	Super.prototype = Animal.prototype;
	Cat.prototype = new Super();  //将实例作为子类的原型。
})();

let cat = new Cat();
console.log(cat.name);		//Tom
cat.sleep();		//Tom正在睡觉
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true

Cat.prototype.constructor = Cat;	//修复构造函数

  1. es6 extends: es6的extends原理就是基于寄生组合继承实现的。
  2. 原型继承: 原型继承就是基于已有的对象来创建新的对象。实现原理就是向函数中传入一个对象,然后返回一个以这个对象为原型的对象。缺点与原型链继承相同:只能继承父构造函数的原型对象上的成员,不能继承父构造函数的实例对象的成员。

使用Object.create方法实现继承。

  let parent4 = {
    name: "parent4",
    friends: ["p1", "p2", "p3"],
    getName: function() {
      return this.name;
    }
  };

  let person4 = Object.create(parent4);
  person4.name = "tom";
  person4.friends.push("jerry");

  let person5 = Object.create(parent4);
  person5.friends.push("lucy");

  console.log(person4.name); // tom
  console.log(person4.name === person4.getName()); // true
  console.log(person5.name); // parent4
  console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
  console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]

1234567891011121314151617181920

因为Object.create实现的是浅拷贝,所以对于引用类型,存在修改数据混乱的问题。

五、原型式继承-普通对象

这里不得不提到的就是 ES5 里面的 Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。 即使不自定义类型也可以通过原型实现对象之间的信息共享。

function object(person) {
 function F() {}
 F.prototype = person
 return new F()
}

let person = {
 name:'张三',
 colors:['red','blue']
}

let person1 = object(person)
person1.colors.push('green')
let person2 = object(person)
person1.colors.push('yellow')
console.log(person) //['red','blue','green','yellow']

适用环境: 你有一个对象,想在它的基础上再创建一个新对象。你需要把这个对象先传给object() ,然后再对返回的对象进行适当修改。类似于 Object.create()只传第一个参数的时候,本质上就是对传入的对象进行了一次浅复制,缺点就是新实例的属性都是后面添加的,无法复用。

六、寄生式继承

与原型式继承比较接近的一种继承方式是寄生式继承,类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。 在原型继承的基础上增加一些方法。

缺点:无法实现函数的复用。

function object(person) {
 function F() {}
 F.prototype = person
 return new F()
}
function createAnother(original){
	let clone = object(original); // 通过调用函数创建一个新对象
	clone.sayHi = function() { // 以某种方式增强这个对象
	console.log("hi");
};
	return clone; // 返回这个对象
}

ES6 与 ES5 继承的区别

ES6中有类class的概念,类 class的继承是通过extends来实现的,ES5中是通过设置构造函数的prototype属性,来实现继承的。

ES6 与 ES5 中的继承有 2 个区别,第一个是,ES6 中子类会继承父类的属性,第二个区别是,super() 与 A.call(this) 是不同的,在继承原生构造函数的情况下,体现得很明显,ES6 中的子类实例可以继承原生构造函数实例的内部属性,而在 ES5 中做不到。

ES5的继承是通过prototype和构造函数机制来实现。ES5的继承是先创建子类的实例对象,然后再将父类的方法添加到this上。

function parent(a, b) {
  this.a = a;
  this.b = b;
}

function child(c) {
  this.c = c
};

parent.call(child, 1, 2)
// 使用call绑定其实是实现了如下代码:
// child.prototype = new Parent(1, 2)
console.log(child);

ES6的继承机制是先创建父类的实例对象this,然后再调用子类的构造函数修改this。

class Parent {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
}

class child extends Parent {
  constructor(a, b, c) {
    // super(a, b)
    this.c = c
  }
}

const c = new child(1, 2, 3)
console.log(c);

可以看到:ES5的继承原理是先创建子类元素child的实例对象,然后再把父类元素parent的原型对象中的属性赋值给子类元素child的实例对象里面,从而实现继承;ES6引入了class的概念,父类首先实例化出来,再修改子类构造函数中的this实现继承。