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 借用构造函数
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实例重新定义一遍,从而实现了继承。
解决了第一种继承方式的两大弊端:
-
引用类型属性的共享。现在每个子类实例的属性都是借用父类构造函数新定义的,因此相互独立。
-
无法向父类构造函数动态传值。现在向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实现继承的一种比较理想的方式。