JavaScript继承、原型和原型链

JavaScript中的继承、原型、原型链,以及类的创建

1. 继承

JavaScript中有6种继承方式:基本继承、借用构造函数、组合继承、原型式继承、寄生式继承、寄生组合式继承

1.1 基本继承

function Parent() {}
Parent.prototype.method1() {}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child

即通过将Parent的一个实例直接作为子类Child的原型对象,所有Child实例都通过__proto__指向这个Parent实例。

弊端:

  1. 原型对象中的所有引用类型的值会被子类共享

  2. 无法在构造子类的时候向父类的构造函数动态传值

因此,这种继承方式几乎很少单独使用,但当父类只是定义了一些方法时,仍然可以使用该方式。

1.2 借用构造函数

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

// 使用new来创建一个示例
const parent = new Parent('Alan Wei', 26)

// new的实现原理类似如下函数
function newObj(constructor, ...args) {
    const tmp = {}
    tmp.__proto__ = constructor.prototype
    constructor.call(tmp, ...args)
    return tmp
}
const parent2 = newObj(Parent, 'Alan Wei', 26)

思考一下,既然构造函数可以用来构造空对象,为什么不能用来构造子类实例呢?比如:

function Child(name, age) {
    Parent.call(this, name, age)
}
const child = new Child('Alan Wei', 26)

此时,我们“借用”了Parent的构造函数作为构造规则,为Child创建了实例(使用new调用Child时,内部的this就是一个Child实例)。现在我们在Parent构造函数中定义的所有属性和方法都会给当前Child实例重新定义一遍,从而实现了继承。

解决了第一种继承方式的两大弊端:

  1. 引用类型属性的共享。现在每个子类实例的属性都是借用父类构造函数新定义的,因此相互独立。

  2. 无法向父类构造函数动态传值。现在向Child传入的参数又被传给了父类构造函数,因此该问题也得到了解决。

缺点:

父类的所有属性和方法都需要重新构造一遍,导致方法无法被共享,每个子类都需要维护一个相同的方法,失去了继承的本质。

因此,我们可以考虑融合上面两种方法的优点,创造一种更好的继承方式。

1.3 组合继承

组合使用上述两种继承方式,来实现方法的共享和属性的独立构造。通用习惯就是,把属性放在父类的构造函数中,把方法放在父类的原型上。

function Child(name, age) {
    Parent.call(this, name, age)
}
Child.prototype = new Parent()
// Child中定义自己的constructor指向,否则会自动通过原型链使用父类原型对象上的constructor
Child.constructor = Child

但这种方式并非完美,实际上需要调用父类构造函数两次:

function Child(name, age) {
    Parent.call(this, name, age)  // 第二次调用Parent的构造函数
}
Child.prototype = new Parent()  // 第一次调用Parent的构造函数
Child.constructor = Child

const child = new Child('Alan Wei', 26)

既然子类会有Parent构造出来属性,必然屏蔽Child原型对象上Parent实例属性,那为什么还要浪费时间和内存去构造它呢?为了解决这个问题,又衍生出下面的继承方式。

1.4 原型式继承

与基本继承的原型式继承的不同之处在于,这是一种更“干净”的实现,它只继承父类的原型,而不继承父类构造函数中的属性(及可能存在的方法)。

function createObj( prototype ) {
    // 重建一个空构造函数
    function F(){}
    // 将该构造函数的原型链替换为传入的原型对象
    F.prototype = prototype
    // 创建并返回一个空对象,但该对象的__proto__指向传入的prototype
    return new F()
}
// child是个空对象,但可以借助原型链访问Parent的原型属性和方法
const child = createObj(Parent.prototype)

上面这种方式在ES5中得到了原生实现,即Object.create()方法,可以传入一个原型对象进去,构造一个以该原型为原型的空对象,这在某些轻量级的继承场景中是十分便捷的:

const child = Object.create(Parent.prototype)
child.name = 'Alan Wei'
child.age = 26

该继承方式显然有着自己的缺陷,但它却为解决组合继承的重复构造问题提供了思路。在介绍如何通过原型式继承来解决组合继承遇到的问题之前,我们再介绍另外一种继承方式 - 寄生式继承,它是原型式继承的工厂化版本。

1.5 寄生式继承

在上面的原型式继承中,我们创建了一个空的子类对象之后,需要手动为其添加自有的属性和方法,如:

const child = Object.create(Parent.prototype)
child.name = 'Alan Wei'
child.age = 26

上述三条语句都是用于构造子类对象的,但却是独立的,我们认为这样耦合性较差,封装程度不够,因此我们通常会将其封装为一个函数:

function createChild(Parent) {
    const child = Object.create(Parent.prototype)
    child.name = 'Alan Wei'
    child.age = 24
}
const child = createChild(Parent)

当我们定义了上述函数之后,每次构造一个子类对象,只需要写下面的一行语句即可,代码看上去也优雅了很多,这就是所谓的寄生式继承。所以,寄生式继承就是原型式继承的一个工厂化(将一系列流程封装在一起,进行快速批量生产)版本。

显然寄生式继承并不是为了解决原型式继承的问题而存在的。接下来我们就来了解一种更加优雅的继承方式 - 寄生式组合继承。

1.6 寄生组合式继承

思路:用原型式继承替换组合继承中的基本继承

function Parent(name, age) {
    this.name = name
    this.age = age
}
Parent.prototype.method1 = function() {}

function Child(name, age) {
    Parent.call(this, name, age)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child

const child = new Child('Alan Wei', 26)

目前,寄生组合式继承被认为是JavaScript实现继承的一种比较理想的方式。

版权

本作品采用 CC BY-NC-ND 4.0 授权。