面向对象(六):JavaScript中的7种继承方式

315 阅读9分钟

JavaScript 对象系列

面向对象(一):认识对象

面向对象(二):认识JavaScript中对象的原型

面向对象(三):创建多个对象的方案

面向对象(四):掌握原型链

面向对象(五):ES6 类的基本使用

面向对象(六):JavaScript中的7种继承方式

面向对象(七):ES6的class转ES5的源码阅读

第六篇

闲聊

JavaScript中有7中继承方式,你会哪几种呢?

  1. 原型链继承
  2. 借用构造函数继承
  3. 原型链 + 借用构造函数组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承
  7. ES6的class继承

到了 2022 年,普遍都是支持 ES6 新语法的浏览器,那么肯定是 class 继承是最香的啦。其他的继承方式怎么办呢?还是了解一下比较的好,哈哈哈~~~


在正题开始之前,先问自己两个问题:

  1. 继承解决了什么问题?为什么要使用继承?
  2. 如何实现继承?

问题一:

为什么要使用继承,一张图片给你答案。

15_01.png

不同的类,存在大量的重复代码,需要封装,需要继承。

问题二:

一段代码,两张图给你答案。

 // 父类
 function Person() {
   this.name = "父类";
 }
 Person.prototype.running = function () {
   console.log(this.name + " running~~");
 };
 ​
 // 子类
 function Coder() {
   this.level = "初级前端工程师";
 }
 Coder.prototype.showLevel = function () {
   console.log(this.level);
 };
 ​
 var coder = new Coder();

上面代码,存在两个类(Person、Student),那么它们在内存中的表现如何:

15_02.png

类与类之间相互独立,毫无相关。那么应该怎么产生关联呢?

15_06.png 看图说话,需要创建一个中间对象,该中间对象的作用:

  1. 子类的函数原型对象指向中间对象
  2. 中间对象的原型指向父类的函数原型对象

最后的结果就是子类和父类间接挂钩,继承也就在此诞生。

问了自己两个问题之后,搞清楚之后,正题就可以开始了。

原型链继承

将子类的函数原型(prototype)指向父类的函数原型(prototype),这就是原型链继承。

 Coder.prototype = Person.prototype // 是这样吗?

当然不是。这样写的话,针对一个子类也还行,但是存在多个子类,都是使用了同一个原型(即父类的原型),那么子类与子类之间就会相互影响。那么应该怎么写呢?

 Coder.prototype = new Person() // 正确写法
 ​
 // 拆分来看
 var obj = new Person()  
 Coder.prototype = obj

创建了一个实例对象Obj, Obj就是所谓的中间对象,那么子类的原型指向它,子类与子类之间就不会相互影响。

完整代码

 function Person(name) {
   this.name = name
 }
 Person.prototype.play = function () {
   console.log("play");
 };
 ​
 function Coder(name, age) {
   // 这里的name不好处理
   this.age = age;
 }
 ​
 Coder.prototype = new Person(); // 关键步骤
 Coder.prototype.eat = function () {
   console.log("eat");
 };
 ​
 var coder = new Coder('copyer', 12)
 console.log(coder) // Person {  age: 12 } 类型错误,应为Coder
 coder.eat()        // eat
 coder.play()       // play

缺点

  1. 参数不好传递(子类的name不好传递到父类中)
  2. 子类的实例对象类型错误,继承的name属性也没有显示

类型错误的原因:Coder.prototype.constructor.name,由于本身丢弃,中间对象又没有constructor属性,就会寻找到Person的函数原型上,所以类型为Person

借用构造函数继承

子类直接调用父类函数。

 function Person(name) {
   this.name = name
 }
 Person.prototype.play = function () { }
 ​
 function Coder(name, age) {
   Person.call(this, name)
   /*
   借用了父类的代码:
   this.name = name
   */
   this.age = age
 }
 ​
 var coder = new Coder('copyer', 18)
 console.log(coder)  // Coder { name: 'copyer', age: 18 }
 // 报错
 coder.play() //  coder.play is not a function

优点

解决了传递参数的问题

缺点:

仅仅继承了父类的属性,并没有继承原型上的方法。

原型链 + 借用构造函数组合继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象)。

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数。

  • 因为函数可以在任意的时刻被调用,因此通过apply()和call()方法也可以在新创建的对象上执行构造函数。
 //父类
 function Person(name) {
   this.name = name;
 }
 Person.prototype.play = function () {
   console.log("play");
 };
 ​
 //子类
 function Coder(name, age) {
   Person.call(this, name)  // 步骤一:借用构造函数
   this.age = age;
 }
 ​
 Coder.prototype = new Person(); // 步骤二:原型链继承
 Object.defineProperty(Coder.prototype, "constructor", {  // 纠正实例对象的类型
   enumerable: false,
   configurable: true,
   writable: true,
   value: Coder,
 });
 ​
 var coder = new Coder('copyer', 18)
 ​
 console.log(coder) // Coder { name: 'copyer', age: 18}
 coder.play() // play

优点:

  1. 解决了传递参数的问题
  2. 解决了子类的类型不对的问题(指定子类的constructor)

缺点:

  • 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候。
  • 父类型中的属性(示例:name属性)会有两份: 一份在中间对象中, 一份在子类型实例中

原型式继承

这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的 一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承)。

怎么实现继承?需要中间对象,最开始我们就已经提及了。

那么是不是可以自己定义一个对象,使其__proto__指向另外一个对象呢?

可以定义一个函数,该函数接受一个对象作为参数,函数体内部创建一个新的对象,使其新对象的原型指向参数对象,然后返回新的对象。实现类似功能的函数,被称为原型式继承函数

实现函数的三种方式:

 // 方式一:借用构造函数原型的指向(最早使用的方法)
 function createObject1(obj) {
     function Fn() {}
     Fn.prototype = obj  
     return new Fn()
 }
 ​
 // 方式二: 利用Object.setPrototypeOf可以改变原型 
 function createObject2(obj) {
   var newObj = {};
   Object.setPrototypeOf(newObj, obj);  // newObj的原型指向obj
   return newObj;
 }
 ​
 // 方式三:利用Object.create(),本质上内部代码实现跟方式一的实现是一样的 (最新版本ECMAScript提供)
 const newObj = Object.create(obj)

既然有了原型式继承函数,那么就可以实现对象与对象之间的继承,类与类之间的继承了。

案例一:对象与对象之间的继承

 const Person = {
   name: "父类",
   play: function () {
     console.log("play~~");
   },
 };
 const obj = Object.create(Person);
 console.log(obj); // {}
 console.log(obj.__proto__); // { name: '父类', play: [Function: play] }

案例二:类与类之间的继承

替换到原型链继承的一行代码即可

 Coder.prototype = new Person()
 ​
 // 修改成:
 Coder.prototype = Object.create(Person.prototype)

优点:

创建中间对象简单,容易实现继承

缺点:

必须与其他方式的继承才能实现完整的继承(自己瞎编的)

寄生式继承

寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(Douglas Crockford)提出和推广的;

为什么需要寄生式继承?

当使用原型式继承的时候,会有如下的缺点(对象与对象之间的继承):

var Person = {}

var p1 = Object.create(Person)
p1.name = 'copyer'
p1.age = 12

var p2 = Object.create(Person)
p2.name = 'james'
p2.age = 35

...

当创建多个对象的时候,就需要写大量的重复性代码。就需要把相同的代码封装成一个函数,该函数就被称为寄生式继承函数

函数的实现:创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回。

 // 寄生式继承函数
 function createObject(name, age) {
     var p = Object.create(Person)
     p.name = name
     p.age = age
     p.play = function() {}
     return p
 }
 ​
 // 然后调用
 var p1 = createObject('copyer', 12)
 var p2 = createObject('james', 35)

createObject函数是不是跟工厂函数一样啊。

所以:寄生式继承是由原型式继承工厂函数的组合套餐。

寄生组合式继承

为什么叫这个名字,我也不清楚,没有达到见名知意效果。

在前面的原型链+借用构造函数的组合继承中,继承已经是比较理想了(不考虑性能的话)。那么为什么还需要寄生组合继承呢?

就是性能方面考虑,需要解决组合继承的两个缺点:

  • 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候。
  • 父类型中的属性会有两份: 一份在中间对象中, 一份在子类型实例中

寄生组合式继承就是为了解决这两个问题。

 // 父类
 function Person(name, age) {
   this.name = name;
   this.age = age;
 }
 Person.prototype.running = function () {
   console.log("running~~");
 };
 ​
 // 子类
 function Coder(name, age, level) {
   Person.call(this, name, age); // 借用构造函数继承(关键步骤一)
   this.level = level;
 }
 //关键步骤二:Coder的原型指向创建出来的中间对象
 Coder.prototype = Object.create(Person.prototype); // 省去了第二次调用构造函数
 // 为了使子类的实例对象的类型正确
 Object.defineProperty(Coder.prototype, "constructor", {
   enumerable: false,
   configurable: true,
   writable: true,
   value: Coder,
 });
 Coder.prototype.write = function () {
   console.log("write~~");
 };
 ​
 var coder = new Coder("copyer", 12, "初级前端工程师");
 ​
 console.log(coder); // Coder { name: 'copyer', age: 12, level: '初级前端工程师' }
 coder.write(); // write~~
 coder.running(); // running~~

优点

  1. 父类只会被调用一次
  2. 继承属性只会保留在实例对象上,并不会出现在中间对象中。

缺点:

暂无(代码量大???)

class继承

ES6 中推出了class新语法特性,其继承只需要一个关键词extends即可。

 // Student类 继承 Person类
 class Person {
   constructor(name, age) {
     this.name = name;
     this.age = age;
   }
   play() {
     console.log("play");
   }
 }
 ​
 class Student extends Person {
   constructor(name, age, address) {
     super(name, age);
     this.address = address;
   }
   running() {
     console.log("running");
   }
 }
 ​
 var s = new Student("copyer", 18, "cq");

优点:

代码量少,思路清晰,便于理解。

缺点:

浏览器兼容性的问题。ie10及以下都不支持。

小小推荐一波,我写的另外一篇,class语法被转化成ES5的代码,最后发现class就是寄生组合式继承。感兴趣的话,可以看看。

ES6的class转ES5的源码阅读

总结

本篇的知识点,是否需要掌握呢?

从基础角度来说,需要彻底掌握;因为这是面对对象的一个重要特性,面对对象又是一个很重要的思想,所以需要掌握。

从开发角度来说,了解即可;因为无论是react开发,还是vue3开发,现在都是函数式编程,面向对象的思想基本用不上(不是绝对哈)。

哈哈哈,最后的结论就是,根据自身的水平即可(基础好的,肯定掌握;基础不好的,也不用太过认真)。