谈谈js中的继承

 

写这篇的目的很简单,巩固一下基础。

从苏州到南京好不容易空下来一段时间。把之前的课程学完,顺便把基础再砸牢一点。 在javascript中继承常见的就以下几种:

原型链继承

原型链的构建是同过一个类型的实例赋值给另一个类型的原型实现的。

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

function SubType() {}

SubType.prototype = new SuperType() // 将子类的原型指向SuperType

var instance = new SubType()

缺点: 1 包含引用类型值的原型会被所有实例共享。

function SuperType() {
  this.color = ['red', 'blue', 'pink'] // 该构造函数的color属性为一个引用类型Array
}

function SubType() {}
SubType.prototype = new SuperType()

var instance1 = new SubType()
instance1.color.push('black')
console.log(instance1.color) // ['red', 'blue', 'pink', 'black']

var instance2 = new SubType()
console.log(instance2.color) // ['red', 'blue', 'pink', 'black'] 

2 在创建子类的实例的时候,不能向超类型的构造函数中传递参数。(注:在软件术语中,被继承的类一般称为“超类”,也有叫做父类。)

借用构造函数继承

因为使用原型链继承存在一些弊端,所有又出现了一种叫借用构造函数的技术(也叫伪对象或经典继承)。 例子

function SuperType(name) {
  this.name = name
  this.color = ['red', 'blue', 'pink'] // 该构造函数的color属性为一个引用类型Array
}

function SubType() {
  SuperType.call(this, 'snoop') // 相对于第一种方法,这里可以传递给父类参数
}

var instance1 = new SubType()
var instance2 = new SubType()
var instance3 = new SubType()

每次创建新的实例的时候都会执行子类型构造函数中的方法SuperType.call来调用父类做初始化,这样一来
每个实例都会拥有自己的属性,不会产生像原型链继承多个实例共享一个引用类型属性的现象。

从上面看,结构函数的思想就是在子类型构造的内部通过call()或者apply()去调用超类型构造函数。

缺点: 在构造函数中定义方法,就不存在函数复用性。而且如果在超类型构造函数的原型中定义方法,对子类型而言是无用的,结果就是所有类型只能使用构造函数模式。

组合继承

为了解决函数复用性的问题,就又出了个组合继承。将原型链继承和构造函数继承和合在一起使用。

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

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

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

SubType.prototype = new SuperType() // 将Subtype的原型指向SuperType的实例,这样以来就可以用到超类型构造函数原型中的方法。

SubType.prototype.constructor = SubType

SubType.prototype.sayAge = function() { // 还可以继续为prototype中定义新的方法
  console.log(this.age)
}

var instance1 = new SubType('snoop1', '12')
var instance2 = new SubType('snoop2', '13')

console.log(instance1.sayName(), instance2.sayName()) // 'snoop1' 'snoop2'
console.log(instance1.sayAge(), instance2.sayAge()) // '11' '12'

组合继承比构造函数继承多做的事情就是 将子类型的prototype指向父类SuperType的实例,然后又在 该原型上定义新的方法sayAge,这样以来 每个不同的SubType实例都可以拥有自己的属性不说,还可以 使用相同的方法。就很舒服~

缺点: 但是也有一定的缺点,就是会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次创建子类型实例 的时候也会通过子类型构造函数内部调用一次超类型构造函数

寄生组合式继承

为了避免两次调用超类型构造函数,就出来一种叫寄生组合式继承。 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本模式如下:

function inheritPrototype(subType, superType) {
  var prototype = Obeject.create(superType.prototype) // 其实就是拷贝一个superType的原型做副本 创建对象
  prototye.constructor = subType // 斧正constructor 增强对象
  subType.prototype = prototype // 赋值到sybType的原型 指定对象
}

这样来做的话就可以弥补之前的为子类型创建原型的步骤了。做到了只调用一次超类型构造函数就是创建超类型对象副本的时候。并且原型链还能保持不变;

ES6 的Class 其实本质就是个原型的语法糖 依然是借助构造函数来实现继承, 后面会专门再梳理一篇关于Class的笔录

ES6 extends

es6 提供了新的创建对象继承的方式 就是通过 Classextends来创建类 和 继承类。 ES6中的class其实是构造函数的语法糖,所以它具有构造函数的一切优点和缺点

class Persion {
  constuctor(name) {
    this.name = name
  }
}

class Jack extends Persion {
  constructor(name) {
    super() // super 在用this关键字之前需要调用super()
    this.name = name
  }
}

创建类的时候 和 es5 的写法有些不同, 通过class去创建 内部有个construcotr() {this.x = } 构造函数来添加属性。

在继承的时候 用extends关键字 更直观的表示出继承。但是通过构造器去创建属性的时候 先调用super(), 这是因为 子类没有自己的this对象, 而super做的事情就相当于是Persion.prototype.constructor.call(this, props) Super虽然代表父类的构造函数,但是返回的是子类实例。也就是super内部的实例指向的是 Jack。

注意点: 当 super 用于对象使用的时候 super指向的是 父类.prototype 也就是在子类的构造函数中super.c即是父类.prototype.c 通过 super 调用父类的方法时,super 会绑定子类的 this。 例子

class Persion {
  constuctor() {
    this.name = 'persion1'
  }
  a() {
    return this.name
  }
}

class Jack extends Persion {
  constructor() {
    super()
    this.name = 'persion2'
    super.name = 'persion3'
  }
  b() {
    super.a()  // 等于 Persion.prototype.a() 也就是调用Persion定义好的a方法
  }
}

let Jack1 = new Jack()
Jack1.b() // is 'persion3' not 'persion2'

es6继承 与我们之前的 构造函数和原型继承的区别: 前者: 先创建一个子类的实例,内部调用超类构造函数

es6:先通过super继承父类的构造函数,改变this, super用作对象时指向父类的prototype来实现继承

有个需要注意的地方⚠️: 严格模式!!! class是ES6的语法ES6默认class块级内是严格模式,这就会带来一个问题 this的问题,举个例子:

window.name = 'snoop'
class A {
  constructor() {
    this.name = 123
  }
  getA() {
    return this.name + 1 // 这个地方的this默认指向类的实例对象,如果单独提取出来作为表达式来用!将会改变this的指向及运行环境,class内部是严格模式。this指向undefined必然报错
  }
}
let child = new A()
let func = child.getA
func() // error: name is undefined