@回顾《JS高程》基础JavaScript继承-各种继承方式已经优点和缺点

235 阅读5分钟

继承

原型链继承

原理: 实例能够访问原型(prototype)上的属性和方法。

本质: 父类(构造函数)重写 子类(构造函数)的原型(prototype),使得子类的实例有能力(通过原型链 --- 隐式原型)来父类的属性和方法。

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property
}

function SubType() {
  this.property = false;
}

SubType.prototype = new SuperType()

// 等价于下面两句
// SubType.prototype.property = new SuperType().property
// SubType.prototype.getSuperValue = new SuperType().getSuperValue

// 注意: 重新添加 SubType.prototype 属性的时候,我们不能使用对象字面量的形式。字面的形式是重新 SubType.prototyp。

var instance = new SubType()

// instance 实例能够通过隐式原型`__proto__`访问 SubType 原型上的属性和方法。
instance.property // false
instance.getSuperValue() // false

默认的原型

Object.prototype 是默认的原型,在默认的原型上挂载的属性和方法

  • constructor
  • hasOwnProperty
  • isPrototypeOf
  • propertyIsEnumberable
  • toLocalString
  • toString
  • valueOf

最顶层的原型 null

Object 的顶层还有原型,就是 null。

存在的问题

  1. 引用数据类型的互相影响。

应用类型的数据在发生变化的时候,所有对齐引用的类型,都会发生变化。

  1. 参数传递的受阻。

我们不能传递参数给父构造函数,意味着原型链继承的灵活性很差。

借用构造函数

原理: 使用 call 方法改变 this 的指向问题.

子类构造函数的 this 通过 Function.prototype.call 方法改变指向,指向父类的构造。

call 的背后:通过在 this 的属性上添加一个父类方法,然后调用,调用之后将父类方法删除(也是如何实现一个call的本质)。

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

function SubType () {
  SuperType.call(this)
}

优点1:没有了引用类型干扰

这样实现的继承,解决了原型继承的第一个问题,引用类型数据的变化之后相互干扰。

原因: 每一个实例都是新的,深拷贝了:this.colors = ['red', 'blue', 'green']; 形成了一个单独的实例。

let ins1 = new SubType()
ins1.colors.push('black')
cnsole.log(ins1.colors) // ['red', 'blue', 'green', 'black']

let ins2 = new SubType()
cnsole.log(ins2.colors) // ['red', 'blue', 'green']

优点2:call 方法也能传递参数

function SuperType (name) {
  this.name = name
}

function SubType () {
  SuperType.call(this"Mg-")
  this.age = 27
}

var ins = new SubType()
console.log(ins.name) // Mg-
console.log(ins.age)  // 27

问题

第一:函数的复用性,没有使用原型链,也是失去了复用性

第二:父类的原型的不可见性,子类根本不知道父类的哪些原型属性和方法。

组合继承

原理: 组合原型链 + 借用构造函数

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

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

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

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

SubType.prototype = new SuperType()

// 修该构造函数
SubType.prototype.constructor = SubType
console.log(SubType.prototype)

// 实例两个子类,没有了之前的缺点
const ins1 = new SubType('aaa', 29)
ins1.colors.push('black') // ['red', 'blue', 'green', 'black]
ins1.sayName() // aaa
ins1.sayAge() // 29

const ins2 = new SubType('bbb', 27)
console.log(ins2.color) // ['red', 'blue', 'green']
ins2.sayName() // bbb
ins2.sayAge() // 27

测试工具

  • instanceof
  • isPrototypeOf

问题

SuperType 构造函数会执行两次,第一次在子类构造函数内部,第二次在给子类原型赋值。

原型模式

ES5 规范化了原型式继承

  • Obejct.create()

原理:

function create(o) {
  function F(){}
  F.prototype = o
  return new F()
}

特点:创建出来的实例引用类型共享。这也是一个问题

var person = {
  name: 'aaa',
  firend: ['bbb', 'ccc', 'ddd']
}

var ano = Object.create(person)
ano.name = 'ttt='
ano.friends.push('one-two')

var oth = Object.create(person)
ano.name = 'ggg-'
ano.friends.push('three-four')

console.log(person.friends) // ['bbb', 'ccc', 'ddd', 'one-two', 'three-four']

Object.create 的第二个参数

var person = {
  name: 'aaa',
  firend: ['bbb', 'ccc', 'ddd']
}

// 相当于一个属性描述符
var ano = Object.create(person, {
  name: {
    value: 'Mag'
})

寄生式继承

特点:类似于工厂模式,又含有原型模式

// 原型模式
function create(o) {
  function F(){}
  F.prototype = o
  return new F()
}

// 工厂函数中包含寄生的函数
function AnotherCreate(obj) {
  var clone = create(obj) // {}, 挂载于原型上
  clone.sayHi() {
    console.log('hi')
  }
  return clone;
}
var person = {
  name: 'aaa',
  firend: ['bbb', 'ccc', 'ddd']
}
var a = AnotherCreate(person)
a.sayHi()

这种寄生式,扩展式的,是依附于别的对象的属性和方法,创建出来就有了别人的属性和方法。

特点 create 函数也不是必须的,任何能够返回一个对象的都可以。

组合继承模式

原理:实例继承实例的部分,原型继承原型的部分,分开继承,同时父类构造函数不在继承两次。

// 核心函数:
function inheritPrototype(SubType, SuperType) {
  var prototype = create(Super.prototype)
  prototype.constructor = SubType

  SubType.prototype = prototype;
}
// create
function create(o) {
  function F(){}
  F.prototype = o
  return new F()
}

// 实现一个继承
function Ass(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

Ass.prototype.sayName = function () {
  console.log(this.name)
}

function Bcc(name, age) {
  Ass.call(this, name)
  this.age = age
}

inheritPrototype(Bcc, Ass)

Bcc.prototype.sayAge = function () {
  console.log(this.age)
}

在 ES5 时代基本上就是使用这种寄生组合的方式来实现继承的问题。

回顾

  • ES5 中没有类的直接概念,ES6 才有了 class 的语法糖。有了更好的类的写法。

  • 原型链继承,使用原型链继承是JS实现继承的底层知识,所以原型链时必须掌握的。但是引用类型的数据共享,传递参数的阻碍缺点有特特别明显

  • 借用构造函数,使用函数的call方法,函数的call的特性,能够改变函数的调用时的运行环境,能够添加参数,和实例的引用类型是深拷贝。弥补了原型链继承的缺点,但是也失去了原型链,共享的特性。

  • 组合继承模式,是将原型链和借用构造函数组合,取长补短,但是也有新的问题,就是父类构造函数的会执行两次。

  • 原型式继承,es5 的 Object.create 方法就基于 一个对象创建另一个对象的这种原型式的继承方法,特点就是依赖于一个已经存在的对象,我们是依赖。但是缺点也是很明显,依赖一个存在的对象,另外,引用类型的数据(浅复制)的问题。

  • 寄生式继承,结合了原型式继承和寄生模式,特点还是依附于一个对象,但是能够自己添加自己的方法。同原型式继承,没有构造函数,没有太多自定的东西。

  • 寄生式组合继承,其实就结合和寄生模式和借用构造模式的结合,借用构造函数管理实例部分的继承,寄生式组合继承管理原型部分。