JS中实现继承的几种方法及其优缺点

178 阅读4分钟

原型链

要搞懂继承,首先得清楚原型链:每一个实例对象都有一个__proto__属性(隐式原型)指向原型,在JS内部用来查找原型链;每一个构造函数都有自己的prototype属性(显示原型),用来显示和修改对象的原型,实例.__proto__ = 构造函数.prototype = 原型。原型链的特点就是:通过实例.__proto__查找原型上的属性,从子类一直向上查找对象原型的属性,继而形成一个查找链即原型链

继承方法及其优缺点

1.原型链继承

使用原型链继承时,主要利用sub.prototype = new super()将父类的实例作为子类的原型,这样连通了子类——父类原型——父类。 核心:将父类的实例作为子类的原型。

function Person() {
  this.name = "jackson";
  this.colors = ["red","blue"];
}
// 给父类原型属性上绑定方法
Person.prototype.sayColors = function() {
  console.log(this.colors);
}
// 子类
function Child() {
  this.height = "1.88"
}
// 实现继承
Child.prototype = new Person();
// 给子类添加特有的方法,注意顺序在继承之后
Child.prototype.sayName = function() {
  console.log(this.name);
}

let child1 = new Child();
child1.sayColors();// [ 'red', 'blue' ]
child1.sayName();  // jackson
  • 优点:可继承构造函数的属性,父类构造函数的属性,父类原型的属性
  • 缺点:无法向父类构造函数传参;且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化
let child1 = new Child();
let child2 = new Child();  
child1.sayColors();// [ 'red', 'blue' ]
child1.colors.push("green");
child2.sayColors();   // [ 'red', 'blue', 'green' ]

2.盗用构造函数继承

基本思路:在子类构造函数中使用callapply来调用父类构造函数。 核心:使用父类的构造函数来增强子类,子类中运行了父类函数中的所有初始化代码,结果就是每个实例都会有自己的colors属性(实现了属性私有化)。

function SuperType() {
  this.colors = ["res","blue"];
}
function SubType() {
  SuperType.call(this);   // 继承SuperType
}
let instance1 = new SubType();
instance1.colors.push("pink");
console.log(instance1.colors);    // [ 'res', 'blue', 'pink' ]

let instance2 = new SubType();
console.log(instance2.colors);    // [ 'res', 'blue' ]
console.log(instance2 instanceof SubType);    // true 实例只是子类的实例
console.log(instance2 instanceof SuperType);  // false 不是父类的实例
  • 优点:解决了子类实例共享父类引用属性的问题,实现了子类属性私有化;创建子类实例时,可以向父类传递参数。
  • 缺点:只能继承父类的实例属性和方法,不能继承原型属性/方法;无法实现函数复用,每个子类都有父类实例函数的副本,影响性能。

3.组合继承

基本思路:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现复用,又可以让每个实例都有自己的属性。

function SuperType(name) {
  this.name = name;
  this.colors = ["res","blue"];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}
function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

let instance1 = new SubType("jackson", 20);
instance1.colors.push("pink");
console.log(instance1.colors);    // [ 'res', 'blue', 'pink' ]
instance1.sayName();  // jackson
instance1.sayAge();   // 20

let instance2 = new SubType("alice",18);
console.log(instance2.colors);    // [ 'res', 'blue' ]
instance2.sayName();  // alice
instance2.sayAge();   // 18
console.log(instance2 instanceof SubType);    // true 实例是子类的实例
console.log(instance2 instanceof SuperType);  // true 实例也是父类的实例
  • 优点:可以继承实例属性/方法,也可以继承原型属性/方法;可以传递参数;函数可以复用;不存在引用属性共享问题,每个实例都有自己的属性。
  • 缺点:调用了两次父类构造函数,生成了两份实例(子类实例将父类原型上的那份屏蔽了)

4.原型式继承

原理:类似Object.create()方法,用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象,结果是将子对象的__proto__指向父对象。

let Person = {
  name : "jackson",
  score: ["80","90","88"]
}
function object(o){
  function F(){};
  F.prototype = o;
  return new F();
}
let child = object(Person);
child.score.push("100");
console.log(child.score);   // [ '80', '90', '88', '100' ]  

适用于你有一个对象,想在它的基础上再创建一个新对象。你需要先把这个对象传给Object(),然后再对返回的对象进行适当的修改。

  • 缺点:共享引用类型

5.寄生式继承

基本思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createObject(obj) {
  let clone = object(obj);   //通过调用函数创建一个新对象
  clone.sayHi = function(){  //给对象添加方法
    console.log("Hi");
  } 
  return clone;   // 返回这个对象
}
let person = {
  name: "jackson",
  score: ["80","90","88"]
}
let anotherPerson = new createObject(person);
console.log(anotherPerson.name);  //jackson
anotherPerson.sayHi();  //Hi
  • 优点:可以添加属性和方法
  • 缺点:给对象添加的函数会导致函数难以复用,与构造函数模式类似

6.寄生组合式继承

基本思路:不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inheritPrototype(subType, superType) {
  let prototype = object(subType.prototype);  // 获得父类原型的一个副本
  prototype.constructor = subType;    
  subType.prototype = prototype;  // 将返回的新对象赋值给子类原型
}
function Person() {
  this.name = "jackson";
  this.colors = ["red","blue"];
}
Person.prototype.sayColors = function() {
  console.log(this.colors);
}
function Child(name, age) {
  Person.call(this, name);    
}
inheritPrototype(Child, Parent);

父类构造函数会被调用两次:一次是在创建子类原型时,另一次是在子类构造函数中调用。

参考文档: 文档1文档2