JavaScript重要概念之七种继承方式

320 阅读9分钟

继承

概念

继承是面向对象软件技术当中的一个概念,与多态,封装共同为面向对象的三个基本特征。
继承可以使子类具有父类的属性和方法或者重新定义,追加属性和方法等。

方式

原型的继承

分析

由于原型链的存在,实例对象可以通过原型链向上查找属性和方法,就可以使用到原型对象的属性和方法。
实例对象和原型对象是相对的,原型对象也有它自己的原型对象。
因此要通过原型链继承,只需要将子类对象,加入到父类对象的原型链中即可。
怎么加入? 只需要将子类的原型对象 等于 父类对象的实例即可。

关键

子类的原型为父类的一个实例对象(根据原型链的定义,子类可以访问到就可以访问到子类的所有方法和属性)

代码

// 声明父类构造函数
function Person(name,age){
	this.name = name
  this.age = age
}
// 在构造函数的原型对象上添加公有方法
// 为什么要添加在Person的原型对象上?
// 当然也是可以直接添加在构造函数中,但是这样做,会导致每次实例化对象都会重新创建方法和属性
// 所以一般将公有属性和方法 添加到构造函数的原型上,节省 内存
// 具体参照构造函数的知识
Person.prototype.getName = function(){
	return this.name
}
// 声明子类构造函数
function Student(price){
  this.price = price
}
// 实现原型链继承
// 子类对象的原型(子类构造函数的prototype属性) 等于父类的一个实例即可
Student.prototype = new Person()
// 实例化子类对象
var s1 = new Student(1500)
console.log(s1)
 

特点

  • 父类新增方法和属性,子类都可以访问得到
  • 简单,易于实现

缺点

  • 从打印s1,即可知道,s1的name,age都是undefined,即使用原型链的继承缺点在于 创建实例时无法向父类构造函数传递参数。
  • 要想为子类对象新增属性和方法,必须等到Student.prototype = new Person() 之后,不能放到构造函数中。

构造函数的继承

分析

为了解决原型链继承的问题,我们考虑使用构造函数的方式。
我们要使用父类的方法和属性,我们可以借用父类的构造函数,在子类的构造函数中实现一遍,就可以解决传递参数的问题。

关键

在子类构造函数中,使用call借用父类的构造函数,实现一遍父类的构造函数。

代码

// 声明一个父类构造函数
function Person(name,age){
	this.name = name
  this.age = age
}
//添加公有方法
Person.prototype.getName = function(){
	return this.name
}
// 声明子类构造函数
// 在子类构造函数中借用父类构造函数
function Student(price,name,age){
	// 子类自己的属性
  this.price = price
  // 借用父类构造函数
  Person.call(this,name,age)
}
// 实例化子类对象
var s1 = new Student(100,'Tom',12)

特点

  • 解决了原型链继承中不能传值的问题

缺点

  • 这种方式只是部分的继承,如果父类的原型上有属性和方法,子类是拿不到这些方法和属性的。
  • 只能继承实例方法,不能继承原型方法
  • 实例化出来的对象,只是子类的实例,并不是父类的实例
  • 无法实现函数复用,每个实例中都有父类中的函数的副本,影响性能

构造函数和原型的组合继承

分析

既然原型链继承和构造函数继承都各有优缺点,那能不能结合起来。
既通过call借用父类方法,继承父类的属性并保留传参的优点,然后通过父类实例作为子类原型,实现函数复用。

关键

  • 借用父类构造函数方法
  • 父类实例作为子类原型

代码

// 声明一个父类构造函数
function Person(name,age){
	this.name = name
  this.age = age
}
//添加公有方法
Person.prototype.getName = function(){
	return this.name
}
// 声明子类构造函数
function Student(price,name,age){
	// 借用父类构造函数方法
  Person.call(this,name,age)
  // 修改子类原型为父类实例
  Student.prototype = new Person()
  // 组合继承 需要修复构造函数指向
  Student.prototype.constructor = Student
  /*
  为什么要修复 constructor指向
  1. 补充constructor知识
  任何一个对象都有一个constructor属性,指向它的构造函数。
  任何一个原型对象都有一个constructor属性,指向它的构造函数(属性值为构造函数的引用)
  Person.prototype.constructor = Person
  任何一个实例对象也有一个constructor属性,默认调用的是它的原型的constructor的值。
  2. Student.prototype = new Person() 执行这一步的时候,就已经替换掉 子类的原型对象,
  可以这么理解:
  上面的代码可以替换成这两句  var p1 = new Person()   Student.prototype = p1
  当实例化子类对象 s1 的时候 var s1 = new Student() 假如这个时候,我们要去找 s1的
  constructor属性,由上面的知识可得,s1.constructor === s1.__proto.constuctor
  即s1.constructor === Student.prototype.constructor, 因为我们修改了子类构造函数为
  父类的实例,所以 s1.constructor === p1.constructor,因为p1是父类构造函数的实例对象,
  所以p1.constructor指向Person() ,即s1.constructor === Person()。
  这与我们上面的知识是违背的( 任何一个对象都有一个constructor属性,指向它的构造函数。)
  所以我们要修复 子类构造函数的指向,即Student.prototype.constructor = Student。
  */
  // 添加子类原型方法
  Student.prototype.sayHello = function(){}
}
// 实例化子类对象
var s1 = new Student(100,'Tom',11)
console.log(s1)

特点

  • 可以继承实例属性和方法,也可以继承原型属性和方法,融合了原型链继承和构造函数继承的优点,是js中常用的继承模式
  • 能做到函数复用

缺点

无论在什么情况下,都要调用两次父类构造函数,一次是创建子类原型的时候,一次是在子类构造函数的内部。生成了两份实例。1. Person.call(this,name,age) 这是第一次 2. Student.prototype = new Person() 这是第二次

组合继承优化1

分析

为了解决生成两份父类实例的缺点,我们可以尝试让子类的原型指向父类的原型,这样就能避免生成两份实例。

代码

// 声明父类构造函数
function Person(name, age) {
   this.name = name
   this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
    console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () { }
}
// 将子类的原型指向父类的原型
Student.prototype = Person.prototype
// 修复constructor指向
Student.prototype.constructor = Student
Student.prototype.sayHello = function () { }
// 实例化子类对象
var s1 = new Student('Tom', 20, 15000)
console.log(s1)

缺点

父类构造函数的原型对象会受影响

Student.prototype.constructor = Student
// 我们在做这一步的时候,这样写是有问题的,因为这样写,实际上把Person.prototype.constructor的值
// 也修改成Student了
// 因为在 Student.prototype = Person.prototype这一步的时候,Student.prototype和Person.prototype
// 指向的就是同一个对象了,任何对这个对象的修改,都会同步反应
// 所以修改之后
Person.prototype.constructor = Student

组合继承优化2

分析

由于直接继承父类的prototype存在缺点,所以,利用一个空对象作为中介。
空对象几乎不占内存。

代码

// 声明父类构造函数
function Person(name, age) {
   this.name = name
   this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
    console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () { }
}
// 这是新的构造函数
var F = function(){}
// 将构造函数F的原型对象指向父类构造函数的原型
F.prototype = Person.prototype
// 将子类构造函数的原型指向 F的一个实例
Student.prototype = new F()
// 修复constructor指向
Student.prototype.constructor = Student
// F是空对象几乎不占内存,这时候修改Student的原型对象就不会影响到父类的原型对象,
// 也不会重复实例化父类

Object.create()继承

分析

组合继承优化2 中的方式,其实可以使用Object.create()方法代替。

关键

var b = Object.create( A ),以对象A为原型,生成了b对象。b对象继承A对象的所有属性和方法。

代码

// 声明父类构造函数
function Person(name, age) {
   this.name = name
   this.age = age
}
//添加原型方法
Person.prototype.setAge = function () {
    console.log("111")
}
// 声明子类构造函数
function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () { }
}
// 核心代码
// 以父类的原型为父对象,生成一个子对象,子对象继承父对象的所有方法和属性
// 又将子对象的原型设置为 这个对象
Student.prototype = Object.create(Person.prototype)
// 修复 constructor指向
Student.prototype.constructor = Student

特点

这是目前来说,最完美的继承方案了

class 继承

分析

ES6 引入了类的概念,并提供extends关键字 实现继承。还可以通过static关键字定义静态方法。
这比ES5 通过修改原型链 实现继承 更加方便。

对比: ES5继承和ES6继承对比(this先后)(需要再理解)

  • ES5的继承实际上是先创建子类的实例对象this,然后再通过父类的构造方法添加到this上(call),即:先有子类的this,再调用父类构造方法修改this。(先有子类的this)
  • ES6的继承机制则完全不同,实质上,是先将父类的属性和方法添加到this上,所以必须先调用super(this),再用子类的构造函数修改this,即先拿到父类的this,再去添加子类的属性和方法。(先有父类的this)

代码

class Point(){}
class ColorPoint extends Point{
  // constructor属性 指向构造函数
	constructor(x,y){
  	super(x,y) // 调用父类的构造函数
    this.color = color
    // 重写子类的toString
    toString(){
    	return this.color+ '' + super.toString() // 调用父类的toString 
    }
  }
}
// super 表示父类的构造函数,用来新建父类的this对象

注意

  • 如果子类没有定义constructor方法,这个方法会默认添加。任何子类都有一个constructor方法,在子类的构造函数中,只有调用super() 方法,才能使用this关键字,否则会报错。
  • 这是因为子类实例的创建,依赖于父类的this,只有先调用super方法。