聊一聊各种继承方式的前世今生

本文旨在让读者掌握 JS 6 大继承方式及其优缺点,了解其演变。详细介绍了类式、构造函数、组合、原型、寄生、寄生组合继承,分析了各自的优缺点及特点,指出寄生组合继承解决了组合继承调用两次父类构造函数的问题,ES6 继承是其语法糖。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

阅读目标

  1. 掌握JS6大继承方式及其优缺点
  2. 了解各种继承方式的演变

类式继承

// 父类
function Parent(name, houses) {
  this.name = name;
  this.houses = houses;
}
Parent.prototype.getName = function() {
  console.log(this.name);
}
Parent.prototype.getHouses = function() {
  console.log(this.houses);
}

// 子类
function Son() {
}

/*==== 核心代码 ====*/
Son.prototype = new Parent('Mark', ['北京']);

const son1 = new Son();
son1.houses.push('上海');
son1.name = '斯特里';
son1.getName() // 斯特里
son1.getHouses(); // ['北京','上海']

const son2 = new Son();
son2.getName() // Mark
son2.getHouses(); // ['北京','上海']

优点

  1. 可复用父类属性

缺点

  1. 父类引用属性会被共享
  2. 子类构建实例时不可向父类传递参数
  3. 子类的constructor属性被修改(不影响,可忽略)

备注

该方式的本质就是修改子类的原型对象,没什么可聊的

构造函数继承

// 父类
function Parent(name, houses) {
  this.name = name;
  this.houses = houses;
}
Parent.prototype.getName = function() {
  console.log(this.name);
}
Parent.prototype.getHouses = function() {
  console.log(this.houses);
}
// 子类
function Son(name, houses) {
  /*==== 核心代码 ====*/
  Parent.call(this, name, houses);
}

const son1 = new Son('斯特里', ['北京']);
son1.houses.push('上海');
console.log(son1.name); // 斯特里
console.log(son1.houses); // [ '北京', '上海' ]
son1.getHouses || console.error('getHouses不存在'); // getHouses不存在

const son2 = new Son('Mark', ['北京']);
console.log(son2.name); // Mark
console.log(son2.houses); // ['北京']
son2.getHouses || console.error('getHouses不存在'); // getHouses不存在

优点

  1. 父类引用属性不会被共享
  2. 子类构建时可向父类传递参数

缺点

  1. 无法继承父类原型链上的属性

备注

  1. 该方式简单粗暴的理解如下:每次创建子类对象的同时会创建一个父类对象,并将父类对象上的属性绑定到子类对象的上下文(this)上。
  2. 再认真观察之前两种继承方式的优缺点,发现彼此的优点正好可以弥补对方的缺点,由此衍生出第三种继承方式,组合继承。

在这里插入图片描述

组合继承

function Parent(name, houses) {
  this.name = name;
  this.houses = houses;
}
Parent.prototype.getName = function() {
  console.log(this.name);
}
Parent.prototype.getHouses = function() {
  console.log(this.houses);
}

function Son(name, houses) {
  /*==== 构造函数继承核心代码 ====*/
  Parent.call(this, name, houses);
}
/*==== 类式继承核心代码 ====*/
Son.prototype = new Parent();

const son1 = new Son('斯特里', ['北京']);
son1.houses.push('上海');
son1.getName();  // 斯特里
son1.getHouses(); // [ '北京', '上海' ]

const son2 = new Son('富贵', ['北京']);
son2.getName(); // 富贵
son2.getHouses(); // [ '北京' ]

优点

  1. 可复用父类属性
  2. 父类引用属性不会被共享
  3. 子类构建实例时可以向父类传递参数

缺点

  1. 会调用两次父类的构造函数

备注

怎么还有缺点,不着急后续还有解决方案

原型继承

function Book() {
  this.name = 'Js book';
  this.alikeBook = ['css book'];
}
Book.prototype.getName = function() {
  console.log(‘getName执行了’);
}
/*==== 核心函数 ====*/
function inheritObject(SubClass) {
  function F() {} // 子类;
  F.prototype = SubClass.prototype;
  return new F();
}

let newBook = inheritObject(Book);
newBook.getName(); // getName执行了
console.log(newBook.name); // undefined

特点

  1. 见名知意,该方式可以仅仅继承父类上的原型属性,不继承父类本身的属性

备注

  1. 该继承其实就是类式继承的一次改造
  2. 乍一看,特别容易得出一种结论:这不就是封装了一下原型模式么。但当认真分析函数后会发现,这种继承方式其实是在原型继承基础上有选择的继承了父类原型上的内容

寄生继承

/*==== 原型继承核心 ===*/
function inheritObject(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
/*==== 寄生继承核心 ====*/
function createBook(obj) {
  const o = inheritObject(obj);
  o.getName = function() {
    console.log(this.name);
  }
  return o;
}

let book = {
  name: 'Js book',
  alikeBook: ['css book'],
}

let newBook = createBook(book);
newBook.name = 'Ajax book';
newBook.alikeBook.push('xml book');
newBook.getName(); // Ajax book
console.log(newBook.alikeBook); // [ 'css book', 'xml book' ]

let otherBook = createBook(book);
otherBook.name = 'flash book';
otherBook.getName(); // flash book
console.log(otherBook.alikeBook); // [ 'css book', 'xml book' ]

特点

  1. 对原型继承产生的对象进行扩展

备注

  1. 该继承其实就是原型继承的一次改造, 寄生继承是的寄生对象是原型继承的产物
  2. 在此我们观察inheritObject函数,每次使用寄生继承都会创建一个空函数F,这样有些浪费内存,此时可以将该函数做成一个单例函数F以避免频繁创建,当完成这一步,我们便实现了ES6中Object.crate()方法

在这里插入图片描述

寄生组合继承

/*==== 原型继承核心 ====*/
function inheritObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
/*==== 寄生继承核心 ====*/
function inheritPrototype(subClass, superClass) {
  let p = inheritObject(superClass.prototype);
  p.constructor = subClass; // 纠正子类constructor的指向
  subClass.prototype = p;
}

function SuperClass(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

SuperClass.prototype.getName = function() {
  console.log(this.name);
}

function SubClass(name, time) {
  /*==== 构造函数继承核心 ====*/
  SuperClass.call(this, name);
  this.time = time;
}

inheritPrototype(SubClass, SuperClass);

let instance1 = new SubClass('Js book', 2014);
let instance2 = new SubClass('css book', 2013);
console.log(instance1.colors);
console.log(instance2.colors);
instance2.getName();

特点

  1. 解决了组合继承中父类构造函数会调用两次的问题

备注

  1. ES6中的class继承就是该继承方式的一种语法糖
  2. 该继承是寄生寄生和组合继承的的融合,通过寄生继承中只操作原型的方式实现原型继承,而不是类式继承中创建对象的方式,从而避免了父类构造函数调用两次的问题

总结

其实分析前五种继承方式的过程就是寄生组合继承的推导过程,推导过程如下:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值