Skip to main content

JavaScript 中的继承方式

继承是 OO 语言中一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际方法。由于 ECMAScript 中函数没有签名,所以只支持实现继承,其实现继承主要是依靠原型链来实现。

6种继承方式的联系

P.S.虚线表示辅助作用,实线表示决定性作用

原型链继承

思想:拿父类实例来充当子类原型对象。

function Parent() {
this.name = 'parent'
this.arr = [1]
}

function Child() {
// ...
}

Child.prototype = new Parent() //核心

var c1 = new Child()
var c2 = new Child()
c1.name = 'child'
c1.arr.push(2)
alert(c1.name) // child
alert(c2.name) // parent
alert(c1.arr) // 1, 2
alert(c2.arr) // 1, 2

优点:

  1. 简单,易于实现

缺点:

  1. 修改c1.arrc2.arr也变了,因为来自原型对象的引用属性是所有实例共享的。可以这样理解:执行c1.arr.push(2);先对c1进行属性查找,找遍了实例属性(在本例中没有实例属性),没找到,就开始顺着原型链向上找,拿到了c1的原型对象,一搜身,发现有arr属性。于是给arr末尾插入了 2,所以c2.arr也变了
  2. 创建子类实例时,无法向父类构造函数传参

P.S.所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的,即所有函数的默认原型都是Object的实例,默认函数都会包含一个内部指针,指向Object.prototype

P.S.确定原型和实例的关系可以通过两种方式:instanceofisPrototypeOf()

借用构造函数继承

思想:在子类构造函数的内部调用父类的构造函数,等于把父类的实例属性复制了一份给子类实例装上。

function Parent(name) {
this.name = name
this.arr = [1]
this.fun = function () {
// ...
}
}

function Child(name) {
Parent.call(this, name) // 核心
// ...
}

var c1 = new Child('c1')
var c2 = new Child('c2')

c1.arr.push(2)
alert(c1.name) // c1
alert(c2.name) // c2
alert(c1.arr) // 1, 2
alert(c2.arr) // 1

优点:

  1. 解决了子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类构造函数传参

缺点:

  1. 无法实现函数复用,每个子类实例都持有一个新的fun函数,太多了就会影响性能,内存爆炸

组合继承(最常用)

思想:通过Parent.call(this),实现对实例属性的继承并保留能传参的优点;通过Child.prototype = new Parent(),实现对父类原型属性和方法继承,达到复用。

function Parent(pName) {
// 只在此处声明基本属性和引用属性
this.name = pName
this.arr = [1]
}
// 在此处声明函数
Parent.prototype.fun = function () {}

function Child(cName) {
Parent.call(this, cName) // 核心1
// ...
}
Child.prototype = new Parent() // 核心2

var c1 = new Child('c1')
var c2 = new Child('c2')

alert(c1.fun === c2.fun) // true

优点:

  1. 不存在引用属性共享问题
  2. 可传参
  3. 函数可复用

缺点:

  1. 子类原型上有一份多余的父类实例属性,而子类实例上的那一份屏蔽了子类原型上的
  2. 父类构造函数被调用了两次,一次是在创建子类原型的时候,另一次是在子类构造函数内部,又是内存浪费

寄生组合继承(最佳方式)

思想:去掉组合继承中为了指定子类原型而调用父类构造函数的这一步,使用寄生式继承来继承父类的原型,然后再将结果指定给子类的原型。

function objectCreate(o) {
// 核心1
var F = function () {}
F.prototype = o
return new F()
}
function Parent(pName) {
// 只在此处声明基本属性和引用属性
this.name = pName
this.arr = [1]
}
// 在此处声明函数
Parent.prototype.fun = function () {}

function Child(cName) {
Parent.call(this, cName) // 核心2
// ...
}
var proto = objectCreate(Parent.prototype) // 核心3
proto.constructor = Child // 核心4
Child.prototype = proto // 核心5

var c1 = new Child('c1')
alert(c1.name)
alert(c1.arr)

优点:完美

缺点:无

function objectCreate(o) {
function F() {}
F.prototype = o
return new F()
}
function inheritPrototype(parent, child) {
var prototype = obj(parent.prototype) // 创建对象
prototype.constructor = child // 增强对象
child.prototype = prototype // 指定对象
}

function Parent(pName) {
this.name = pName
this.arr = ['brother', 'sister', 'parents']
}
Parent.prototype.run = function () {
return this.name
}

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

inheritPrototype(Parent, Child) // 通过这里实现继承

var test = new Child('trigkit4', 21)
test.arr.push('nephew')
alert(test.arr) // brother,sister,parents,nephew
alert(test.run()) // 只共享了方法

var test2 = new Child('jack', 22)
alert(test2.arr) // 引用问题解决

*原型式继承

思想:借助原型基于已有的对象创建新对象,同时不必创建自定义类型

function objectCreate(o) {
// 核心1
var F = function () {}
F.prototype = o
return new F()
}

var parent = {
name: 'parent',
arr: [1]
}
var c1 = objectCreate(parent) // 核心2
var c2 = objectCreate(parent)
// 增强
c1.fun = function () {
alert('c1.fun')
}
c1.name = 'c1'
c1.arr.push(2)
c2.arr.push(3)

c1.fun()
alert(parent.arr) // [1, 2, 3]

优点:

  1. 从已有对象衍生新对象,不需要创建自定义类型(指Child构造函数)(从本质上讲,objectCreate()对传入其中的对象执行了一次浅复制)

缺点:

  1. 原型引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
  2. 无法实现代码复用(新对象是现取的,属性是现添的,都没用函数封装,怎么复用)

P.S.ES5提供了Object.create()函数,内部就是原型式继承,IE9+Firefox 4+Safari 5+Opera 12+Chrome支持。 这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。第二个参数与Object.defineProperties()方法的第二个参数格式相同——每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

*寄生式继承

思想:与原型式继承紧密相关,这种继承思路是把原型式继承 + 工厂模式结合起来,目的是为了封装创建的过程。

function objectCreate(o) {
// 核心1
var F = function () {}
F.prototype = o
return new F()
}
function createAnother(obj) {
// 核心2
// 创建新对象
var clone = objectCreate(obj)
// 增强
clone.attr1 = 1
clone.sayHi = function () {
alert('hi')
}
// ...
return clone
}
function Parent() {
this.name = 'parent'
this.arr = [1]
}

var c1 = createAnother(new Parent()) // 核心3

alert(c1.attr1)
c1.sayHi()

优点:

  1. 从已有对象衍生新对象,不需要创建自定义类型

缺点:

  1. 无法实现增强的函数与属性的复用(没用到原型,当然不行)

P.S.objectCreate函数并不是必须的,换言之,创建新对象 -> 增强 -> 返回该对象,这样的过程叫寄生式继承,新对象是如何创建的并不重要

小结

JavaScript 主要通过原型链实现继承。

  • 原型链继承是通过将一个类型的实例赋值给另一个构造函数的原型实现的,这样,子类能访问父类的所有属性和方法,这一点与基于类的继承很相似。原型链继承的问题是对象实例共享所有继承的属性和方法,因此不宜单独使用;
  • 解决上面问题的技术是借用构造函数继承,即在子类构造函数内部调用父类构造函数。这样每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型;
  • 使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性;

此外,还存在下列可供选择的继承模式:

  • 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
  • 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用父类构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
  • 寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。