Javascript:继承的七种方式

135 阅读5分钟

继承的概念

通过某种方式让一个对象可以访问到另一个对象中的属性和方法

使用继承的原因

把对象的方法和函数都放在构造函数中声明会导致内存的浪费

继承的分类

1. 原型链继承
构造函数、原型、实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含有一个指向构造函数的指针,而实例都包含一个原型对象的指针。

继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。

function Person() {
  this.skins = ['white', 'yellow', 'black'];
}

Person.prototype.getSkins = function() {
  return this.skins;
}

function Student() {
  this.nationality = ['China', 'Russia'];
}

Student.prototype = new Person();  // 先继承再为子类实例创建属性/方法

Student.prototype.getNationality = function() {
  return this.nationality;
}

const person = new Person();
const student = new Student();
console.log(person.getSkins());  // ['white', 'yellow', 'black']
console.log(student.getSkins());  // ['white', 'yellow', 'black']

优点:

  • 父类方法可以复用
    缺点:
  • 父类的引用属性会被所有的子类实例共享,多个实例对引用类型的操作会被篡改
const student1 = new Student();
const student2 = new Student();

student1.skins.push('brown');

console.log(student1.getSkins()); // ['white', 'yellow', 'black', 'brown']
console.log(student2.getSkins()); // ['white', 'yellow', 'black', 'brown']
  • 子类构建实例时不能向父类传递参数

2. 借用构造函数继承
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)。

借用构造函数继承方式的核心是call()函数。子类实例调用父类构造函数时,子类实例都会将父类构造函数的属性复制一份,解决了原型链继承中多实例相互影响的问题

function Person() {
  this.skins = ['white', 'yellow', 'black'];
}

function Student() {
  Person.call(this);
}

const student = new Student();
console.log(student.skins) // ['white', 'yellow', 'black']

优点:

  • 父类的引用属性不会被共享
const student1 = new Student();
const student2 = new Student();
student1.skins.push('brown');

console.log(student1.skins) // ['white', 'yellow', 'black', 'brown']
console.log(student2.skins) // ['white', 'yellow', 'black']
  • 子类构建实例时可以向父类传递参数
function Person(name) {
  this.name = name;
}

function Student(name) {
  Person.call(this, name);
}

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

const student = new Student('cc');
student.getName();  // cc

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
Person.prototype.getName = function() {
  return this.name;
}

const student = new Student('cc');
student.getName();  // TypeError: student.getName is not a function
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

3. 组合继承
结合原型链继承借用构造函数继承两种方式即为组合继承。

用原型链继承方式实现对原型属性和方法的继承,用借用构造函数继承方式实现实例属性和方法的继承。

function Person(name) {
  this.name = name;
  this.skins = ['white', 'yellow', 'black'];
}

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

function Student(name, age) {
  Person.call(this, name);  // 父类构造函数复制属性和方法给子类实例
  this.age = age;
}

Student.prototype = new Person();  // 原型链继承父类属性和方法

Student.prototype.getAge = function() {
  return this.age;
}

const student1 = new Student('aa', 21);
student1.skins.push('brown');  // 不会影响到student2的skins属性
console.log(student1);
console.log(student1 instanceof Student);  // true
console.log(student1 instanceof Person);  // true

const student2 = new Student('bb', 18);
console.log(student2);

优点:

  • 父类方法可以被复用
  • 父类引用属性不会被共享
  • 子类构建实例时可以向父类传递参数 缺点:
  • 使用子类创建实例对象时,原型中会存在两份相同的父类实例属性和方法,造成了性能上的浪费

image.png

4. 原型式继承,即浅拷贝
针对两个普通对象,因不是构造函数,所以无法使用构造函数方法实现继承

可以利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

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

object()本质是对传入其中的对象执行了一次浅拷贝,将构造函数F的原型直接指向传入的对象。

const company = {
  name: 'aa',
  business: ['ad', 'software']
}

const c1 = object(company);
c1.name = 'ss';
c1.business.push('ecommerce');
console.log(c1.business); // ['ad', 'software', 'ecommerce']

const c2 = object(company);
c2.name = 'kk';
c2.business.push('video');
console.log(c2.business); // ['ad', 'software', 'ecommerce', 'video']

// c1的business属性被c2的business篡改

在传入一个参数的情况下,object()方法与Object.create()的行为相同。

Object.create()接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。 优点:

  • 父类方法可以复用 缺点:
  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。

image.png

  • 子类构建实例时不能向父类传递参数。

5. 寄生式继承
寄生式继承,即使用原型式继承获得一份目标对象的浅拷贝,然后增强这个浅拷贝的能力。

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

function createAnother(original) {
  // 创建一个新对象
  const clone = object(original);  
  // 增强这个对象
  clone.getName = function() {
    return this.name;
  }
  return clone;
}

const company = {
  name: 'aa',
  business: ['ad', 'software']
}

const c1 = createAnother(company);
c1.name = 'ss';
c1.business.push('ecommerce');
console.log(c1.business);

const c2 = createAnother(company);
c2.name = 'kk';
c2.business.push('video');
console.log(c2.business);

优缺点和原型式继承一样。
优点:

  • 父类方法可以复用 缺点:
  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
  • 子类构建实例时不能向父类传递参数

6. 寄生组合继承
寄生组合继承可以解决组合继承两次调用父类的构造函数造成性能浪费的缺点。
核心在于inheritPrototype(),让子类的prototype指向父类原型的拷贝,这样就不会调用父类的构造函数,引发内存的浪费。

寄生组合继承 是继承的最优解决方案

function inheritPrototype(child, parent) {
  // 修正子类原型对象指针,指向父类原型的一个副本
  child.prototype = Object.create(parent.prototype);
  // 增强对象,弥补因重写原型而失去的默认constructor属性
  child.prototype.constructor = child;
}

function Person(name) {
  this.name = name;
  this.skins = ['white', 'yellow', 'black'];
}

Person.prototype.getSkins = function() {
  return this.skins;
}

function Student(name, age) {
  Person.call(this, name);
  this.age = age;
}

inheritPrototype(Student, Person);

Student.prototype.getAge = function() {
  return this.age;
}

const student1 = new Student('aa', 21);
const student2 = new Student('bb', 22);

student1.skins.push('brown');

console.log(student1);
console.log(student2);
// 借用构造函数继承和组合继承的缺陷:二次调用父类的构造函数
Student.prototype = new Person();
// 改为 =>
Student.prototype = Object.create(Person.prototype);

7. ES6 extends

extends关键字用于类声明或者类表达式中,以创建一个类,该类是另一个类的子类。--MDN

class Person {
  constructor(name) {
    this.name = name;
    this.skins = ['white', 'yellow', 'black'];
  }
  
  getName() {
    return this.name;
  }
}

class Student extends Person {
  constructor(name, age) {
    super(name); //调用超类,获取父类构造函数
    this.age = age;
  }
}

const student1 = new Student('aa', 22);
student1.skins.push('brown');
console.log(student1.getName());  // aa
console.log(student1);

const student2 = new Student('bb', 23);
console.log(student2);

extends实现

function _inherit(child, parent) {
  if (typeof parent !== 'function' && parent !== null) {
    throw new TypeError(`Super expression must either be null or a function, not ${typeof parent}`)
  }
  // 子类原型的__proto__指向父类原型
  child.prototype = Object.create(parent && parent.prototype, {
    // 给子类添加constructor属性
    // child.prototype.constructor === child
    constructor: {
      value: child,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  
  if (parent) {
    Object.setPrototypeOf ? Object.setPrototypeOf(child, parent) : child.__proto__ = parent;
  }
}

本文参考文章: JavaScript 继承的八种写法 - 知乎 (zhihu.com)