JS继承的7种方式,ES5,ES6继承的区别

前言

在js中继承无处不在,因为有原型链的存在,如果对原型链不是很明白的童鞋,可以先看一看 《弄清JS的原型链》;如果不知道继承是如何实现的童鞋,那么本文值得你读一读

目录

一、何为继承,继承的特点

继承:

简单的说就是子类继承父类,子类可以使用父类的方法、属性

特点:

①在JS中都是通过原型实现继承的
②就近原则(如果父类和子类都有某个同名属性时,优先使用最近的属性,也就是子类的属性)

二、ES5的继承实现

1、原型链继承

实现原理:

利用原型让一个引用类型继承另一个引用类型的属性和方法。也就是让子类的原型对象等于父类的实例

优点:

  • 可以继承父类及其原型的全部属性和方法
  • 实例和子类和父类在一条原型链上

缺点:

  • 子类实例无法向父类传参
  • 因为是通过原型实现的继承,所以父类的实例属性会变成子类的原型属性,会导致包含引用类型值的原型属性会被所有的实例共享(基本类型没有该问题)

注意:

  • 给子类添加原型的属性和方法需谨慎,必须放在替换原型的语句之后,否则会被覆盖掉

上代码:

function ParentType() {
   // 父类
   this.attr = true
 }
 ParentType.prototype.getAttr = function () {
   // 向父类原型增加方法
   return this.attr
 }
 function ChildType() {
   // 定义子类
   this.a = false
 }
 ChildType.prototype = new ParentType() // 定义其原型对象等于父类的实例

 let instance = new ChildType() // 创建子类实例

 console.log(instance.attr) // true,调用父类原型中的属性,可以继承到
 console.log(instance.getAttr()) // true,调用父类原型中的方法,可以继承到
 console.log(instance.a) // false,使用子类原型中的属性,可以继承到
 // instance.constructor指向的ParentType,这是因为原来的childType.prototype被重写了的缘故。
 console.log(instance.constructor === ParentType)

2、借用构造函数继承

实现原理:

在子类型构造函数的内部,调用父类型的构造函数。通过call() 和 apply() 方法的作用, 改变this的指向!

优点:

  • 可以向父类传参
  • 因为使用的call或apply的方式,所以可以实现多继承,即一个子类继承多个父类的属性和方法
  • 解决了引用数据类型值共享的问题

缺点:

  • 因为只是“借调”了构造函数,所以无法继承父类原型中的属性和方法
  • 父类的属性和方法都要在父类构造函数中定义才能继承到,每次创建子类实例都重新执行一次父类的构造函数,无法实现方法的复用。(所以在实际开发中,这种继承方式很少单独使用)

注意:

  • 子类的私有属性,为防止被父类覆盖,应定义在call或apply之后

上代码:

function ParentType(name) {
   // 父类
   this.attr = [1, 2]
   this.name = name
 }

 ParentType.prototype.getAttr = function () {
   // 向父类原型增加方法
   return this.attr
 }

 function ChildType(name) {
   // 定义子类
   ParentType.call(this, name) // 继承了父类
 }

 let instance1 = new ChildType('liang') // 创建子类实例
 instance1.attr.push(3)
 console.log(instance1.attr) // [1, 2, 3] 修改了属性值
 console.log(instance1.name) // liang, 实现了向父类传参
 console.log(instance1.getAttr) // undefined,未继承父类原型中的属性和方法

 let instance2 = new ChildType('zai') // 创建另一个子类实例
 console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
 console.log(instance2.name) // zai, 实现了向父类传参

3、组合继承

实现原理:

将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承方式

优点:

  • 解决了原型链的引用类型值共享的问题
  • 解决了借用构造函数无法继承原型以及构造函数中的方法无法被复用的问题
  • 是 JavaScript 最常用的继承模式

缺点:

  • 无论什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型原型的时候(ChildType.prototype = new ParentType();),一次是在子类型构造函数内部(ParentType.call(this, name); )

上代码:

function ParentType(name) {
   // 父类
   this.attr = [1, 2]
   this.name = name
 }
 ParentType.prototype.getAttr = function () {
   // 父类原型的方法
   return this.attr
 }
 function ChildType(name) {
   // 定义子类
   ParentType.call(this, name) // 继承属性
 }
 ChildType.prototype = new ParentType() // 继承方法
 ChildType.prototype.constructor = ChildType // 强化继承

 let instance1 = new ChildType('liang') // 创建子类实例
 instance1.attr.push(3)
 console.log(instance1.attr) // [1, 2, 3] 修改了属性值
 console.log(instance1.name) // liang, 实现了向父类传参
 console.log(instance1.getAttr()) // [1, 2, 3],获取父类原型中的属性和方法

 let instance2 = new ChildType('zai') // 创建另一个子类实例
 console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
 console.log(instance2.name) // zai, 实现了向父类传参

4、原型式继承

实现原理:

道格拉斯·克罗克福德提出了原型式继承方式,借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

核心代码:

function object(o) {
  function F() {} // 先创建一个临时的构造函数
  F.prototype = o; // 将传入的对象作为这个构造函数的原型
  return new F(); // 返回这个临时对象的实例
}

缺点:

  • 因为是重写了原型,所以与原型链继承方式一样,会存在引用类型值共享的问题

注意:

  • 在ECMAScript 5 中新增了Object.create() 方法规范了原型式继承。这个方法接收两个参数:新对象原型的对象,新对象定义额外属性的对象
  • 即上面的object()函数等价于:Object.create(o)
  • 一般是在没有必要兴师动众的去创建一个子类的构造函数时,且不存在引用类型属性时,原型式继承是完全可以胜任的

上代码:

let person = {
   name: 'liang',
   friends: ['zhao', 'qian'],
 } // 用作新对象原型的对象(要浅继承的对象)

 // function object(o) {
 //   function F() {} // 先创建一个临时的构造函数
 //   F.prototype = o // 将传入的对象作为这个构造函数的原型
 //   return new F() // 返回这个临时对象的实例
 // }

 // 上面的object()函数等价于:Object.create(o);
 let p1 = Object.create(person) // 创建一个实例
 p1.name = 'sun'
 p1.friends.push('zhou')

 let p2 = Object.create(person) // 创建另一个实例
 p2.name = 'zheng'
 p2.friends.push('wu')

 console.log(p1.friends) // ["zhao", "qian", "zhou", "wu"]
 console.log(p2.friends) // ["zhao", "qian", "zhou", "wu"]
 console.log(person.friends) // ["zhao", "qian", "zhou", "wu"]

5、寄生式继承

实现原理:

创建一个仅用于封装过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象

缺点:

  • 为对象添加函数,无法做到函数的复用,降低效率,类似于借用构造函数方式

注意:

  • 里面的Object.create()不是必须的,只要是能返回新对象的函数都适用于次模式(比如new)

上代码:

function createPerson(original) {
  let clone = Object.create(original) // 原型式继承
  clone.getName = function () {
    // 为对象添加方法
    return this.name
  }
  return clone
}

let person = {
  // 新对象原型的对象
  name: 'liang',
  frinds: ['zhao', 'qian'],
}

let instance = createPerson(person) // 新对象既拥有person对象的属性和方法,又拥有自己的方法
console.log(instance.getName()) // liang,调用到了新对象自己的方法

6、寄生组合式继承

实现原理:

使用寄生式继承来继承父类的原型,然后将结果指定给子类型的原型

优点:

  • 可以实现子类实例像父类传参
  • 引用类型值不会被共享
  • 实现了函数的复用
  • 只调用了一次父类的构造函数
  • 效率高

总结:

  • 解决了上述所有的缺点
  • 继承组合方式是引用类型最理想的继承方式
  • 解决了组合式继承存在一个最大的问题,会调用两次父类的构造函数

上代码:

function ParentType(name) {
  // 父类
  this.attr = [1, 2]
  this.name = name
}
ParentType.prototype.getAttr = function () {
  // 父类原型的方法
  return this.attr
}
function ChildType(name) {
  // 定义子类
  ParentType.call(this, name) // 继承属性
}

let prototype = Object.create(ParentType.prototype) // 创建一个等于父类原型对象的对象
prototype.constructor = ChildType // 增强对象,弥补因重写原型而失去的constructor属性
ChildType.prototype = prototype // 完成了对父类的属性和方法的继承

let man1 = new ChildType('liang')
man1.attr.push(3)
console.log(man1.name) // liang,继承到父类的属性
console.log(man1.getAttr()) // [1,2,3] 继承到了父类的方法

let man2 = new ChildType('zhao')
console.log(man2.name) // zhao
console.log(man2.getAttr()) // [1,2] 引用类型值没有被共享

三、ES6的继承实现

1、 ES6通过class的extends实现继承

实现原理:

ES6的继承实现方法,实质上是 JavaScript 现有基于原型继承的语法糖,其内部其实也是ES5寄生组合继承的方式,通过call构造函数,在子类中继承父类的属性,通过原型链来继承父类的方法

优点:

  • 更规范——严格模式下执行
  • 可读性高

注意:

  • 如果对class的使用不是很明白的,那么建议先读一读 《es6的class是什么?有什么用》
  • 相比ES5的继承中,子类的__proto__属性指向的对应的构造函数的原型。ES6的Class定义的子类同时有prototype属性和__proto__属性,因此同时存在两条继承链:
    1、子类的__proto__属性,表示构造函数的继承,总是指向父类
    2、子类prototype对象的__proto__属性,表示原型的继承,总是指向父类的prototype对象
  • extends做了两件事情,一个是通过Object.create()把子类的原型赋值为父类的实例,实现了继承方法,子类.prototype.proto__也自动指向父类的原型,一个是手动修改了子类的__proto,修改为指向父类,(本来在es5 中应该是指向Function.prototype)
  • extends也可以继承ES5的构造函数

上代码:

class ParentType {
  // 父类
  constructor(param) {
    // 父类的构造函数
    this.param = param
    this.attr = 'haha'
  }
  static age = 21
  static showSomething(str) {
    // 父类的静态方法
    console.log(str)
  }
  getParam() {
    // 原型的方法
    return this.param
  }
}

class ChildType extends ParentType {
  // 定义子类继承父类
  // constructor(param) {
  //   super(param) // 使子类获取到this对象
  // }
  static showSth(str) {
    super.showSomething(str) // 通过super调用父类的函数
  }
}

let child = new ChildType('123456') // 创建实例

ParentType.showSomething('lalala!') // lalala! 静态方法的使用
// child.showSomething('child!') // Uncaught TypeError: child.showSomething is not a function 实例无法继承静态方法。
console.log('age==>', ChildType.age)
// ChildType.state.age = 23
// ChildType.showSth('yeah!') // yeah! 子类可以继承父类的静态方法。

console.log(child.getParam()) // 123456 调用到了父类的方法
console.log(ChildType.prototype.__proto__ === ParentType.prototype) // true
console.log(ChildType.__proto__ === ParentType) // true

四、ES5,ES6继承的区别

  1. ES6的继承实现方法,实质上是 JavaScript 现有基于原型继承的语法糖,其内部其实也是ES5寄生组合继承的方式
  2. 相比ES5的继承中,子类的__proto__属性指向的对应的构造函数的原型。ES6的Class定义的子类同时有prototype属性和__proto__属性

五、总结

  1. ES6继承的实现,是基于ES5而实现的,class是语法糖,使用上更加规范、严格
  2. es5中实现继承的方法有很多,最常用的是组合式继承、寄生组合式继承
  3. 根据不同的场景,使用不同的方法,如果支持ES6,那么尽量用ES6 class的extends实现继承

觉得本文写的不错的,希望点赞、收藏、加关注,每月不定期更新干货哦,谢谢您嘞!

你可能感兴趣的文章:

上一篇:es5和es6的区别是什么?


下一篇:ES5严格模式