JavaScript继承的实现

74 阅读4分钟

JavaScript继承的实现

继承是面向对象的,使用这种方式我们可以更好地复用以前的开发代码,缩短开发周期、提高开发效率。

一、JS实现继承的几种方式

第一种:原型链继承

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

function Parent() {
  this.name = 'parent'
  this.arr = [1, 2]
}
function Child() {
  this.name = 'child'
}
Child.prototype = new Parent()
const child1 = new Child()
console.log(child1.name, child1.arr)
// 父类的方法和属性都能够访问,但其实有潜在问题
const child2 = new Child()
child2.arr.push(3)
console.log(child2.arr, child1.arr)
// 因为两个实例使用的是同一个原型对象,它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点

第二种:构造函数继承(借助 call)

function Parent() {
  this.name = 'parent'
}
Parent.prototype.getName = function () {
  return this.name
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
let child1 = new Child()
console.log(child1)
// console.log(child1.getName())
// 构造函数实现继承的优点,使父类的引用属性不会被共享,优化了第一种继承方式的弊端
// 不过,只能继承父类的实例属性和方法,不能继承原型属性或方法

第三种:组合继承(前两种方式)

function Parent() {
  this.name = 'parent'
  this.arr = [1, 2]
}
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
let child1 = new Child()
let child2 = new Child()
child1.arr.push(3)
console.log(child1.arr, child2.arr)
// 不互相影响
console.log(child1.getName())
console.log(child2.getName())
// 正常输出'parent'

// Parent执行了两次,第一次是改变Child的prototype的时候,第二次是通过call方法调用Parent的时候,那么Parent多构造一次就多进行了一次性能开销

第四种:原型式继承

ES5的Object.create方法,可以接收两个参数:

  1. 用作新对象原型的对象
  2. 为新对象定义额外属性的对象(可选参数)
let parent = {
  name: 'parent',
  friends: ['p1', 'p2', 'p3'],
  getName: function () {
    return this.name
  },
}
let parent1 = Object.create(parent)
parent1.name = 'tom'
parent1.friends.push('jerry')

let parent2 = Object.create(parent)
parent2.friends.push('lucy')
console.log(parent1.name)
console.log(parent1.name === parent1.getName())
console.log(parent2.name)
console.log(parent1.friends)
console.log(parent2.friends)
// 多个实例的引用类型属性指向相同的内存,存在篡改的可能

第五种:寄生式继承

使用原型式继承可以获得一份目标对象的浅拷贝,然后利用这个浅拷贝的能力再进行增强,添加一些方法,这样的继承方式就叫作寄生式继承

let parent = {
  name: 'parent',
  friends: ['p1', 'p2', 'p3'],
  getName: function () {
    return this.name
  },
}
function clone(original) {
  let clone = Object.create(original)
  clone.getFriends = function () {
    return this.friends
  }
  return clone
}
let parent1 = clone(parent)
console.log(parent1.getName())
console.log(parent1.getFriends())

第六种:寄生组合式继承

结合第四种中提及的继承方式,解决普通对象的继承问题的Object.create方法,我们在前面这几种继承方式的优缺点基础上进行改造,得出了寄生组合式的继承方式,这也是所有继承方式里面相对最优的继承方式

function clone(parent, child) {
  // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
}
function Parent() {
  this.name = 'parent'
  this.arr = [1, 2, 3]
}
Parent.prototype.getName = function () {
  return this.name
}
function Child() {
  Parent.call(this)
  this.friends = 'child'
}
clone(Parent, Child)
Child.prototype.getFriends = function () {
  return this.friends
}
let parent1 = new Child()
console.log(parent1)
console.log(parent1.getFriends())
console.log(parent1.getName())

ES6 的 extends 关键字实现逻辑

我们可以利用ES6里的extends的语法题,使用关键词很容易直接实现JS的基础,但是想深入了解extends语法糖是怎么实现的,就得深入研究extends的底层逻辑

  • 因为浏览器的兼容性问题,如果遇到不支持ES6的浏览器,那么就得利用babel这个编译工具,将ES6的代码编译成ES5,让一些不支持新语法的浏览器也能运行
// class Person {
//   constructor(name) {
//     this.name = name
//   }
//   // 原型方法
//   // 即 Person.prototype.getName = function() {}
//   // 下面可以简写为 getName() {...}
//   getName = function () {
//     console.log('Perosn', this.name)
//   }
// }
// class Gamer extends Person {
//   constructor(name, age) {
//     // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()
//     super(name)
//     this.age = age
//   }
// }
// const asuna = new Gamer('Asuna', 20)
// asuna.getName()

function _possibleConstructorReturn(self, call) {
  // ...
  return call && (typeof call === 'object' || typeof call === 'function')
    ? call
    : self
}
function _inherits(subClass, superClass) {
  // 这里可以看到
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}
var Parent = function Parent() {
  // 验证是否是Parent构造出来的this
  classCallCheck(this, Parent)
}
var Child = (function () {
  _inherits(Child, Parent)
  function Child() {
    _classCallCheck(this, Child)
    return _possibleConstructorReturn(
      this,
      (Child.proto || Object.getPrototypeOf(Child)).apply(this, arguments)
    )
  }
  return Child
})(Parent)