js必须知道的继承

219 阅读4分钟

可以学到什么?

本文希望你能搞清楚哪些问题呢?

  1. 继承是什么意思
  2. 继承的原理
  3. 实现继承的主要方式有哪些,每一个的具体写法以及优缺点
  4. extends关键字继承

继承的定义

字面意思,比如孩子继承了父亲或者母亲的基因的类似意思。js中,简单的说继承就是子类可以使用父级的方法属性,并且可以进行扩展。

原理

es5,我们利用js查找属性时会查找他的原型链的特性实现的继承。es6虽然出现了关键字 extends,来实现类的继承,但是最终编译以后的代码也是es5的代码。

es5的这个特性原型链应该会讲到,但是这里我们简单看个代码来测试一下。

Object.prototype.name = '随风行酱'

function Person() {}
const b = new Person()

console.log(b) // Person {}
console.log(Object.getPrototypeOf(b)) // {}
console.log(b.name) // 随风行酱
console.log(Object.getPrototypeOf(Object.getPrototypeOf(b))) // [Object: null prototype] { name: 'panhe' }

我们知道,在Person构造函数上合原型上都没有name这个属性。但是其对应的实例对象 b可以访问到name属性,就是因为js会沿着实例属性的原型链上去查找,找到Object原型上的name属性的原因。

这大概就是es5继承抓住的特性,即它的原理了。

继承的方式

​ 继承的主要几种方式有:

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生式组合继承
  • es6的extends继承
原型链继承

这种方式就是利用原型链查找的特性,来看下什么是原型链继承

function Person () {
    this.colors = ['red', 'green']
}
Person.prototype = {
    getColors: function() {
        return this.colors
    }
}
// child
function Child () {
}
Child.prototype = new Person()
const c = new Child()

console.log(Object.getPrototypeOf(c)) // {colors: ['red', 'green'] }
console.log(c.getColors()) // ['red', 'green']

Child内部以及原型上都没有getColors 和 colors 2个属性。

但是通过把Child的原型指向Person实例对象,通过原型链就能访问到Parent上的属性和方法了。

第一,能够访问Person构造函数内部的属性是由于 new Person() 产生的对象会包含this的属性。

第二,能够访问,Person原型上的属性,是由于原型链查找

缺点: 子类修改父类实例属性,会造成引用类型属性同步修改的问题

(画图)

构造函数继承

构造函数用的不是原型链继承,更多的是使用new内部作用来实现继承。来看下什么是构造函数继承

function Person () {
    this.colors = ['red', 'green']
}
Person.prototype = {
    getColors: function() {
        return this.colors
    }
}

function Child () {
    Person.call(this, ...arguments)
}

const c = new Child();
console.log(c.colors // ['red', 'green']
console.log(c.getColors()) // undefiend

构造函数继承只继承了父级构造函数类的属性。并没有继承父级原型上的属性。

这种继承原理是:利用new 在创建对象是会生成新的对象,并赋值this到这个对象上的特性。把父级的this搬到了子类上来重新绑定。

由于是一个新对象,所以生成的对象都是全新的,父级的引用类型的值不在公用地址(解决了上面的缺点问题)。

缺点: 不能访问父级原型链上的属性

组合继承

组合继承就是2者的结合,如下:

function Person () {
    this.colors = ['red', 'green']
}
Person.prototype.getColors = function() {
    return this.colors
}

function Child () {
    Person.call(this, ...arguments)
}
Child.prototype = new Person()

可以看出来,2这结合以后,new 一个子类会调用2次父级的构造函数。 其实,就是在原型链基础上覆盖了构造函数内部的属性,来做到不共享引用类型数据的问题。

缺点: 父级构造函数调用2次,并且构造函数内的属性生成了2份。

原型式继承,寄生式继承,寄生组合继承

看图说话:

原型式继承

图的最上面为原型式继承。我们用object函数会生成一个新的以function F为构造函数的对象。生成的对象的__proto__会指向这个被继承的对象。这样就形成了原型链。

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

const cat = {
    heart: '@',
    colors: ['white', 'black']
}
const child = object(cat)

缺点: 引用类型属性同步修改

寄生式继承

寄生式继承就是在原型继承基础上,可以给生成的对象增加属性。

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
function createAnthor(origin) {
    const clone = object(origin)
    clone.sayhi = function() {
        return 'hi'
    }
    return clone
}
const cat = {
    heart: '@',
    colors: ['white', 'black']
}
const child = createAnthor(cat)

缺点: 引用类型属性同步修改

寄生式组合继承

从图最下方可以看书,Child的实例child的__proto__指向了中间代理的对象prototype,然后prototype的__proto__又指向了Parent,这样就形成了原型链了。

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function createAnthor(Child, Parent) {
    const prototype = object(Parent.prototype) // 产生一个对象指向 parent.prototype
    prototype.constructor = Child // 每个原型对象有一个constructor对象
    Child.prototype = prototype
}

function Person() {}
Person.prototype = {}
function Child() {}
Child.prototype = {}
createAnthor(Child, Person)

extends关键字继承

Es6 的继承是由extends实现继承的。其中向下编译结果原理类似组合继承和寄生式组合继承结合。

来看下编译。

前:

class Parent {
    constructor(name) {
        this.name = name
    }
    getname() {
        return this.name
    }
}

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

const c = new Child('随风行酱')
console.log('aa', c.getname())

编译后:

"use strict";

function _inheritsLoose(subClass, superClass) { 
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  _setPrototypeOf(subClass, superClass); 
}

function _setPrototypeOf(o, p) { 
  _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; 
  return _setPrototypeOf(o, p); 
}

var Parent = /*#__PURE__*/function () {
  function Parent(name) {
    this.name = name;
  }

  var _proto = Parent.prototype;

  _proto.getname = function getname() {
    return this.name;
  };

  return Parent;
}();

var Child = /*#__PURE__*/function (_Parent) {
  _inheritsLoose(Child, _Parent);

  function Child(name) {
    return _Parent.call(this, name) || this;
  }

  return Child;
}(Parent);

var c = new Child('随风行酱');
console.log('aa', c.getname());

image-20211117115006071