详解原型与继承

383 阅读4分钟

原型 & 原型链

在进入正题之前我们先来看一个例子,比如说 👇

function Parent() {}

尝试展开 Parent 你就会发现它有这些属性👇

image.png

接着我们利用 Parent 这个构造函数来生成一个对象 🧐

const obj = new Parent()

非常朴实无华的代码,我们试着将它也打印出来

image.png

对比对象和函数打印出来的结果我们可以发现:

  • 函数会有一个 prototype 属性,其中 Parent.prototype 上还挂载了一个 constructor 以及一个 __proto__ 属性。
  • 而对象是没有 prototype 属性的,可以看到它就只有 __proto__

在《JavaScript 高级程序设计 第4版》 中指出:

  • 每次只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性。默认情况下所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数
  • 自定义构造函数时,原型对象默认只会获得 constructor 属性,其他的所有方法都继承自 Object。每次调用的实例,内部 [[Prototype]] 指针会被赋值为构造函数的原型对象。一般来说标准中没有指定的实现方式,但是大部分浏览器中都是通过暴露一个 __proto__ 来实现(比如说 Chrome、Safari、Firefox)

通常我们将 prototype 称之为 显式原型,而 __proto__ 称为隐式原型,而原型与原型之间形成的链接我们称之为原型链。

image.png

上图是 Parent 与它的原型之间的关系,但是 Parent 是有 __proto__ 这个属性的,它应该指向哪里呢?

答案就是 Function.prototype,因为 Parent 是通过 Function 来生成的,因此它的 __proto__ 就是指向 Function.prototype

值得注意的是,Function.__proto__ 是指向 Function.prototype 的,但是 Function.prototype.__proto__ 却指向的是 Object.prototype 😨,事情好像并不简单,但是实际上却很简单,这几个属性的类型可以告诉我们答案👇

image.png

没错,Function.__proto__ 本身就是函数类型,函数本身的原型就会指向 Function.prototype ;而 Function.prototype.__proto__ 则是一个对象,对象的默认原型就是 Object.prototype

到这里,我们就可以画出一份完整的原型链的指向关系了 ☺️👇

image.png 完整的原型链图

继承

原型链继承

原型链继承是一种基于实例共享原型链原理的基础上实现的,以下是简单实现的代码👇

function Parent() {
    this.name = 'jack'
}

Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child() {}
Child.prototype = Parent.prototype

const child = new Child()
child.getName() // 输出 jack

只要属性挂载在 prototype 上,那么继承它的子类的实例就可以访问到它。但是这种方式实现的继承也有一些缺点,比如:

  • 原型链是所有实例共享的,对于原型链上的属性的操作,产生的副作用也会被所有实例共享
  • 无法初始化父类中的属性

构造函数继承

构造函数继承是将父类构造函数放入到子类构造函数中执行,从而实现父类与子类之间的实例属性共享的一种继承方式,代码如下👇

function Parent(name){
    this.name = name
}

function Child(name, age) {
    Parent.call(this, name)
    this.age = age
}

const child = new Child('tony')

这种继承方式也有它的缺点,就是说不能利用原型链来共享父类的方法(废话),必须要写一套在构造函数里面才行。

组合继承

组合继承结合了上面提到的两种继承的继承方式👇

function Parent(name) {
    this.name = name
}

function Child(name, age) {
    Parent.call(this, name)
    this.age = age
}

Parent.prototype.getName = function() {
    console.log(this.name)
}

Child.prototype = new Parent()

const child = new Child('tony', 25)

然鹅,这种继承实现方式也有它的缺陷,我们简单列举一下这种继承方式的缺点👇

  1. 父类构造函数会执行两次
  2. 子类原型会被复写,导致 constructor 指向了父类

原型式继承 && 寄生式继承

《JavaScript 高级程序设计 第4版》中提到了 “原型式继承” 和 “寄生式继承” 这两种继承方式,核心代码👇

function create(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}

可以看出,这种方式的继承只链接了实例和原型,和上面所说的其他继承方式是不太一样的,适合需要共享对象属性的情况,也难怪书中称之为 ”不涉及严格意义上构造函数的继承方法“ 😀。

寄生式组合继承

”寄生式组合继承“ 顾名思义,是结合了 ”寄生式继承“ 和 ”组合继承“ 这两种继承方式的一种实现方式。以下是实现方式👇

function Parent(name) {
    this.name = name
}

function Child(name, age) {
    Parent.call(this, name)
    this.age = age
}

function initPrototype(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}

Child.prototype = initPrototype(Parent.prototype)
Child.prototype.constructor = Child // 修复因为复写 prototype 导致 constructor 指向错误

可以看出,这种实现方式除了 ”稍微有一些复杂“ 之外,已经没有前面几种继承方式的缺点了。

ES6 的继承

ES6 中提供了关键字 class ,实现继承就不需要像之前那么复杂了,只需要通过 extendssuper 这两个关键字即可。其中 extends 代表继承,而 super 则指向父类的构造函数。

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

class Child extends Parent {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}

不过总的来说,ES6 的继承只是一个语法糖,核心还是基于原型与原型链来实现的。