JS核心理论之《继承的多种实现方式》

190 阅读4分钟

在类上添加属性和方法

通过构造函数、原型和. 语法三者都可以在类上添加属性和方法。但是三者是有一定的区别的。

  • 构造函数:通过this添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过this添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。 但是这样创建的好处是即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份)
  • 原型:通过原型继承的方法并不是自身的,我们要在原型链上一层一层的查找,这样创建的好处是只在内存中创建一次,实例化的对象都会指向这个prototype对象, 但是这样做也有弊端,因为实例化的对象的原型都是指向同一内存地址,改动其中的一个对象的属性可能会影响到其他的对象
  • . 语法:在类的外部通过. 语法创建的属性和方法只会创建一次,但是这样创建的实例化的对象是访问不到的,只能通过类的自身访问。

类式继承

又称为原型继承,使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。

Sub.prototype = new Super() ;

缺点:一个子类的实例原型从父类构造函数中继承来的共有属性就会直接影响到其他子类。

示例:

function Super() {
  this.list = [1,'a']
}

function Sub() {
}

Sub.prototype = new Super()
var inst1 = new Sub()
var inst2 = new Sub()
console.log(inst2.list) // [1,'a']
inst1.list.push(2)
console.log(inst2.list) // [1,'a',2]

构造函数继承

构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的。

Super.call(this,id)

直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是单独复制的,所以各个实例化的子类互不影响。

缺点:会造成内存浪费的问题。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中。

示例:

function Super() {
  this.list = [1,'a']
}
Super.prototype.printList = function(){
  console.log(this.list)
}

function Sub() {
  Super.call(this)
}

var inst1 = new Sub()
var inst2 = new Sub()
inst1.list.push(2)
console.log(inst1.list) // [1,'a',2]
console.log(inst2.list) // [1,'a']
inst1.printList()   // TypeError

组合继承

组合继承就是:类式继承+构造函数继承。 在构造函数继承的基础上增加了原型链继承,使得子类能够访问父类的原型方法。同时多个实例化的子类之间互不影响。

缺点:父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍)。

示例:

function Super(name) {
  this.name = name
  this.list = [1,'a']
}
Super.prototype.getName = function() {
  console.log(this.name)
}

function Sub(name,title) {
  Super.call(this,name)
  this.title = title
}
Sub.prototype = new Super()
Sub.prototype.getTitle = function() {
  console.log(this.title)
}

var inst1 = new Sub('inst1','inst1')
inst1.list.push('b')
console.log(inst1.list)    //[1,'a','b']
inst1.getName()            // inst1
inst1.getTitle()           // inst1

var inst2 = new Sub('inst2','inst2')
console.log(inst2.list)    //[1,'a']
inst2.getName()            // inst2
inst2.getTitle()           // inst2

寄生组合继承

组合式继承的方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍),所以为了解决这个问题,又出现了寄生组合继承。 刚刚问题的关键是父类的构造函数在类继承和构造函数继承的组合形式中被创建了两遍,但是在类继承中我们并不需要创建父类的构造函数,我们只是要子类继承父类的原型即可。 所以说我们先给父类的原型创建一个副本,然后修改子类constructor属性,最后在设置子类的原型就可以了。

示例:

function inheritObject(o) {
  function F() {}  //声明一个过渡函数对象
  F.prototype = o  //过渡对象的原型继承父对象
  return new F()   //返回一个过渡对象的实例,该实例的原型继承了父对象
}
function inheritPrototype(Sub,Super) {
  var p = inheritObject(Super.prototype) //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型
  p.constructor = Sub                    //修正因为重写子类原型导致子类constructor属性被修改
  Sub.prototype = p                      //设置子类的原型
}

function Super(name) {
  this.name = name
  this.list = [1,'a']
}
Super.prototype.getName = function() {
  console.log(this.name)
}
function Sub(name) {
  Super.call(this,name)
}
inheritPrototype(Sub,Super)
var inst1 = new Sub('inst1')

console.log(inst1.name)  //inst1
inst1.getName()          //inst1

ES6的继承

es6引入了class、extends、super、static(部分为ES2016标准)

class Super {
  constructor(props) {
    this._title = props.title;
  }
  get title() { return this._title; }
  static staticMethod() {}
  toString() { return `${ this._title }`; }
}

class Sub extends Super {
  constructor(props) {
    super(props);
    this._link = props.link;
  }
  set link(val) { this._link = val; }
  toString() { return `${ this._link }`; }
}

ES6和ES5继承的区别

大多数浏览器的ES5实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。 Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

  1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true