JS继承的八种写法

继承的分类

Js继承大可以根据是否用了Object.create分成两大类。
其中使用Object.create的原型链继承和不使用Object.create的原型式继承相似;类似的,用了Object.create的构造函数继承也与不用Object.create的寄生式继承对应。

而寄生组合继承则是对组合继承做了优化,是最为理想的继承。ES6的extends与它基本一致,只是在实现方式上有所不同。

不用Object.create

原型链

构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function() {
  return this.property
}
function SubType() {
  this.subProperty = false
}
SubType.prototype.getSubValue = function() {
  return this.subProperty
}
// 关键,创建SuperType的实例,让SubType.prototype指向这个实例
SubType.prototype = new SuperType()
let inst1 = new SuperType()
let inst2 = new SubType()
console.log(inst2.getSuperValue()) // true
  • 优点:
    + 可以复用父类方法(Saves Memory)
  • 缺点:
    + 子类构建实例时不能向父类传递参数
    + 所有子类都会共享相同的父类引用属性。一个改,都会变。
function SuperType(){
  this.colors = ["red", "blue", "green"];
}
function SubType(){}

SubType.prototype = new SuperType();

// 多个实例共享父类引用(上面的 new SuperType())
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

构造函数继承

用父类的构造函数增强子类实例。也就是复制父类的实例给子类(没用Prototype)

function SuperType() {
  this.color = ['red', 'green']
}

// 构造函数继承
// 使得每个实例都会复制得到自己独有的一份属性
function SubType() {
  // 将父对象的构造函数绑定在子对象上
  SuperType.call(this)
  // 创建子类实例时调用SuperType构造函数,
  // 于是SubType的每个实例都会将SuperType中的属性复制一份
  // 解决了原型链继承中多实例相互影响的问题。
}

let inst1 = new SubType()

console.log(inst1)

// SubType {color: Array(2)}
  • 优点:(和原型链继承完全反过来)

    • 父类的引用属性不会被共享
    • 子类构建实例时可以向父类传参
  • 缺点:

    • 无法实现复用,每个子类都有父类实例函数的副本,影响性能
    • 只能继承父类的实例属性和方法,不能继承原型属性/方法
...
// 父类原型链上的方法
SuperType.prototype.getColor = function () {
  return this.color
}
console.log(inst1.getColor()) // TypeError: inst1.getColor is not a function

组合继承

把上面俩方法结合起来。用原型链实现对原型属性和方法的继承,用构造函数来实现实例属性的继承。

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

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

function SubType(name, age) {
  // 1、构造函数来复制父类的属性给SubType实例
  // *** 第二次调用SuperType(),给instance1写入两个属性name,color。
  SuperType.call(this, name)
  this.age = age
}

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

// 2、原型继承
// *** 第一次调用SuperType(),给SubType.prototype写入两个属性name,color。
SubType.prototype = new SuperType()
// 手动挂上构造器,指向自己的构造函数 SubType
SubType.prototype.constructor = SubType
SubType.prototype.getAge = function () {
  return this.age
}

let inst1 = new SubType('Asuna', 20)

console.log('inst1', inst1)
console.log(inst1.getName(), inst1.getAge())
console.log(inst1 instanceof SubType, inst1 instanceof SuperType)


// inst1 SubType {name: "Asuna", colors: Array(3), age: 20}
// Asuna 20
// true true
  • 优点: 上面两个的优点合起来
  • 缺点:
    + 调用了两次SuperType()。实例对象inst1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的父类实例的属性/方法。这种被覆盖的情况造成了性能上的浪费。

用Object.create

原型式继承 (就是浅拷贝)

比如现在有两个对象

const Chinese = {
  nation: '中国'
}

const Doctor = {
  career: '医生'
}

现在想要让”医生”去继承”中国人”,也就是说,怎样才能生成一个”中国医生”的对象?

这里要注意,这两个对象都是普通对象,不是构造函数,所以无法使用构造函数方法实现”继承”。

可以用object()方法

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

// ES5中存在Object.create()的方法,能覆盖下面这个方法
function object(obj) {
  function F() { }
  F.prototype = obj
  return new F()
}

上面的函数本质上对传入的obj进行了一次浅拷贝,直接把构造函数F的原型指向了obj。

let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

// ES5中新增了 Object.create() 方法规范化了原型式继承。
// 这个方法接收两个参数:一个用作新对象原型的对象
// 和(可选的)一个为新对象定义额外属性的对象。
// 在传入一个参数的情况下, Object.create()与 object()方法的行为相同
let yetAnotherPerson = object(person)
//  => 
let yetAnotherPerson = Object.create(person)
  • 优点:
    + 可以复用父类方法
  • 缺点:
    + 子类构建实例时不能向父类传递参数
    + 所有子类都会共享相同的父类引用属性。一个改,都会变。

寄生式继承(能附加一些方法

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

优缺点其实和原型式继承一样,寄生式继承说白了就是能在拷贝来的对象上加点方法,也就是所谓增强能力。

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

function createAnother(original) {
  // 通过调用函数创建一个新对象
  let clone = object(original)
  //以某种方式来增强这个对象
  clone.getName = function () {
    console.log('我有了getName方法: ' + this.name)
  }
  return clone
}

let person = {
  name: 'Asuna',
  friends: ['Kirito', 'Yuuki', 'Sinon']
}

let inst1 = createAnother(person)
let inst2 = createAnother(person)

寄生组合继承(最优方案)

组合继承会有两次调用父类的构造函数而造成浪费的缺点,寄生组合继承就可以解决这个问题。

核心在于inheritPrototype(subType, superType),让子类的prototype指向父类原型的拷贝,这样就不会调用父类的构造函数,进而引发内存的浪费问题。

function inheritPrototype(subType, superType) {
  // 修正子类原型对象指针,指向父类原型的一个副本 (用object()也可以) 
  subType.prototype = Object.create(superType.prototype)

// 构造函数继承和组合继承的缺陷:二次调用 SuperType 的构造函数
// subType.prototype = new SuperType()
// 改为 => 
// subType.prototype = Object.create(superType.prototype)

  // 增强对象,弥补因重写原型而失去的默认的constructor属性
  subType.prototype.constructor = subType
}

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

SuperType.prototype.getColors = function () {
  console.log(this.colors)
}

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}

inheritPrototype(SubType, SuperType)

SubType.prototype.getAge = function () {
  console.log(this.age)
}

let inst1 = new SubType("Asuna", 20)
let inst2 = new SubType("Krito", 21)
console.log('inst1', inst1)
console.log('inst2', inst2)

用混入(mixin)实现多继承

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类(就是寄生组合继承的套路)
MyClass.prototype = Object.create(SuperClass.prototype);

// 混合其它类,关键是这里的 assign() 方法
Object.assign(MyClass.prototype, OtherSuperClass.prototype);

// 重新指定constructor
MyClass.prototype.constructor = MyClass;

// 在之类上附加方法
MyClass.prototype.myMethod = function() {
  // do a thing
};

Object.assign 会把 OtherSuperClass原型上的函数拷贝到 MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。Object.assign 是在 ES2015 引入的,且可用 polyfilled。

Extends from ES6

虽然 ES6 引入了关键字 class,但是底层仍然是基于原型的实现。class 只是语法糖,使得在 JavaScript 模拟类的代码更为简洁。

class Person {
  constructor(name) {
    this.name = name
  }

  // 原型方法
  // 即 Person.prototype.getName = function() { }
  // 下面可以简写为 getName() {...}
  getName = function () {
    console.log('Person:', this.name)
  }
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    super(name)
    this.age = age
  }
}

const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

extends实现继承的code

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

总结

  • ES6 Class extends是ES5继承的语法糖
  • JS的继承除了构造函数继承之外都基于原型链构建的
  • 函数声明和类声明的区别
    + 函数声明会提升,类声明不会。首先需要声明你的类,然后才能访问它。
  • 可以用寄生组合继承实现ES6 Class extends,但是还是会有细微的差别
    + ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。

一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends
[JavaScript常用八种继承方案]
(https://juejin.im/post/5bcb2e295188255c55472db0)
ES6中类的实现原理
Javascript面向对象编程(三):非构造函数的继承

posted @ 2020-05-28 00:52  NullCream  阅读(508)  评论(1)    收藏  举报