# 继承

孩子复制父亲的全部能力,并在此基础上对复制的能力进行修改而不影响父亲的能力,我们称之为继承

我们为什么要学习继承呢?因为这是我们实现前端工程化的过程中,一定绕不开的话题,下面我们一步步来了解一下如何实现继承

要理解继承,这里就要先知道,面向对象是什么意思?

# 面向对象 到底什么意思?

对象作为程序的基本单元。将数据操作数据的函数封装成对象,以提高软件的重用性、灵活性和扩展性。
在这个程序里,万物皆是对象,所以就被称为面向对象

面向对象是一种代码规范,你实际开发中可以遵循,也可以不遵循。也不是所有语言都支持面向对象的形式进行开发。

但可以按照面向对象规范进行开发的语言,都能被成为面向对象语言

所以JS也是面向对象语言

# 类 到底是什么意思?

实际上是一种数据类型对象的抽象形式)。每个类包含数据操作数据的函数。类的实例称为对象。

并不是所有的面向对象语言都具有这种数据类型(比如JS就没有...JS在ES6中新增的class只是一种语法糖,不是真的类),而继承则是类设计模式中的一种基本特征

类设计模式的基本特征包括:继承、封装、多态。

JS底层没有这种数据类型,所以也就不是具备类设计模式的特征(所以标题的继承加了引号),JS是一套基于原型链的委托模式(当前没有的功能委托给原型链的上一层,层层委托)。

但由于我们知道了表现形式和设计思路,所以使用一些方法,可以让JS实现近似继承的功能(但会很别扭)。

# JS如何实现“继承”?

JS要实现"继承",需要满足下面几个条件:

  • 子类是父类的一个复本(可以理解为一个快照)
  • 子类修改继承过来的功能,不影响父类(因为子类是快照,所以当然不能影响父类)
  • 子类可以重写/新增功能(随便改写快照)

是图纸,实例是建筑物。
复制一份图纸起到参考作用,具体建筑物造成什么样子,是根据具体的施工条件来进行的。具体施工时会在图纸的基础上进行调整和优化,不会完全按照图纸去实施(当然也可以完全按照图纸去实施,看具体的施工条件)。

下面就来具体实现下JS中的继承

直接上代码(可以直接打开浏览器控制台,直接复制粘贴看结果)

// 方法1 构造函数继承
function Parent() {
  this.name = 'Jack';
}

Parent.prototype.sayHi= function() {
  console.log(`hi! I am ${this.name}~`);
}

function Child() {
  Parent.call(this); // 通过this绑定,把Parent里的属性“继承”到child
}

var child = new Child();

console.log(child.name); // Jack
child.sayHi(); // Uncaught TypeError: child.sayHi is not a function

// 遗留问题:无法继承原型链上的方法
// 方法2 构造函数 + 原型链连接 继承
function Parent() {
  this.name = 'Jack';
}

Parent.prototype.sayHi= function() {
  console.log(`hi! I am ${this.name}~`);
}

function Child() {
  Parent.call(this);
}

Child.prototype = Parent.prototype;

var child = new Child();

console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~

// 这里有个问题,如果Child修改自己的原型链,会影响Parent
Child.prototype.run = function() {
  console.log('run');
}

Parent.prototype.run(); // run

// 遗留问题:修改Child的原型链,会影响Parent
// 方法3 构造函数 + 原型链连接 继承(解决原型链相互影响)
function Parent() {
  this.name = 'Jack';
}

Parent.prototype.sayHi= function() {
  console.log(`hi! I am ${this.name}~`);
}

function Child() {
  Parent.call(this);
}

// 创建一个对象,这个对象的[[prototype]]关联到Parent.prototype,返回这个对象给Child.prototype(创建的对象内容是空的,所以也就没有了constructor)
Child.prototype = Object.create(Parent.prototype);

// 此时Child修改自己的原型链,不会影响Parent
Child.prototype.run = function() {
  console.log('run');
}

Parent.prototype.run(); // Uncaught TypeError: Parent.prototype.run is not a function

var child = new Child();

console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run

// 此时又产生一个新问题...不能准确知道child继承自谁。js实现继承就是有这么多问题...
console.log(child.constructor === Child); // false 此时Child的constructor为空,会顺着原型链往上找,然后针找到了Parent
console.log(child.constructor === Parent); // true

// 遗留问题:Child的实例child,继承信息为Parent
// 方法4 构造函数 + 原型链连接 继承(解决继承信息不准确)
function Parent() {
  this.name = 'Jack';
}

Parent.prototype.sayHi= function() {
  console.log(`hi! I am ${this.name}~`);
}

function Child() {
  Parent.call(this);
}

Child.prototype = Object.create(Parent.prototype);

// 手动给Child添加constructor
Child.prototype.constructor = Child;

Child.prototype.run = function() {
  console.log('run');
}

var child = new Child();

console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run

console.log(child.constructor === Child); // true
console.log(child.constructor === Parent); // false

// 此时使用JS实现了 类似 类设计模式的继承
// 但和真正的继承还是有区别的
// 比如顺着Child的原型链往上,还是能修改Parent
// 方法5 ES6 class 语法糖 版
class Parent {
  constructor() {
    this.name = 'Jack';
  }

  sayHi() {
    console.log(`hi! I am ${this.name}~`);
  }
}

class Child extends Parent {
  constructor() {
    super();
  }

  run() {
    console.log('run');
  }
}

var child = new Child();

console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
console.log(child.constructor === Child); // true
console.log(child.constructor === Parent); // false

// 可以说class的出现,极大的方便了我们去实现继承
// 但他掩盖了 JS 不是类设计模式的本质...
// 但只要我们知道这个问题存在,就不怕去使用。我们能根据现实情况去权衡
// 低版本不支持class的浏览器,polyfill是使用构造函数来实现的
// 方法6 使用JS的链式委托来实现 类似继承
var Parent = {
  name: 'Jack',
  sayHi: function() {
    console.log(`hi! I am ${this.name}~`);
  }
}

var Child = Object.create(Parent);

Child.run = function() {
  console.log('run');
}

var child = Object.create(Child);

console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
// 判断继承自谁
console.log(Object.getPrototypeOf(child) === Child); // true
console.log(Object.getPrototypeOf(child) === Parent); // false


// 不清楚为啥的,可以复习下this啦~
child.name = 'Tony';
child.sayHi(); // hi! I am Tony~
// 不会影响Parent
console.log(Parent.name);
Parent.sayHi();// hi! I am Jack~

// 使用链式委托的方式,实现更简洁,也更符合JS的底层实现
// 熟悉原型链的话,会发现这种方法,相比构造函数,在原型链连接结构上也更简单且清晰(大家可以根据上一章原型链中的思路,动手画一下这两种的链路图)
// 在了解了内部的各种实现原理后,最后如何使用还是看大家喜好

如果觉得内容对你有帮助,请点个关注,你们的鼓励是我持续更新下去的动力,比心。

如需转载请注明出处,感恩。

更多内容可先关注GitHub (opens new window)