【设计模式】JavaScript 中的继承

继承是每个面向对象编程语言都会讨论的话题,只不过在 JavaScript 中,继承变得十分复杂,因为它不像别的语言一样只要简单地通过一个关键字就能实现继承。

不过得益于 JavaScript 的强大,我们一样可以实现继承。

为什么要继承

针对这个问题,我的理解是:主要是为了实现代码的复用,减少重复性代码。

对于已经实现了某些方法的父类,我们再写子类的时候,完全没有必要在重新实现一遍,我们只需要继承父类的方法就行了,这样能大大减少重复性代码。

类式继承

类式继承是模仿其他语言出现的一种继承方法,它得益于 JavaScript 中的原型(prototype)链,因为在 JavaScript 引擎中,在查找对象属性的时候,会沿着原型链一级一级向上查找,直到找到对应的属性或者原型链顶端为止。

类式继承的主要目标是构造函数+原型对象的“类”。

// 父类 Person
function Person(name) {
	this.name = name
}
Person.prototype.getName = function() {
	return this.name
}

// 子类 Author
function Author(name, books) {
    // 属性继承
    Person.call(this, name)
    this.books = books
}
// 方法继承
Author.prototype = new Person()
// 需要重新改写构造函数
Author.prototype.constructor = Author
// 添加新的方法
Author.prototype.getBooks = function() {
	return this.books
}

var author = new Author('断崖', '无')
author.getName() // 断崖 
author.getBooks() // 无

extend 函数

实际使用中,我们不可能每次继承一个类都要写这么一坨代码。

上面代码中,Person 和 Author 是紧耦合的,如果那天需要修改 Author 的继承父类,那么你需要修改两个地方。

而且最重要的是,Person 的实例化是需要一个 name 参数的,但是我们没有传值,因为根本无法传值,如果 Person 实例化时这个 name 是必须的话,那么在这个继承过程中就会报错。

下面我们用一个 extent 函数方法来解决上面的问题。

function extend(subClass, superClass) {
	// 用一个空函数引用父类原型
	// 因为是空函数,所以在构造过程中不会出现因参数问题导致的报错
	var F = function() {}
	F.prototype = superClass.prototype
	// 子类继承
	subClass.prototype = new F()
	// 重设 constructor
	subClass.constructor = subClass
	// 存储父类构造函数
	subClass.superClass = superClass
}

// 定义父类
function SuperCls() {
	this.name = 'Name'
}
SuperCls.prototype.getName = function() {
	return this.name
}

// 定义子类
function SubCls() {
	SubCls.superClass.call(this, arguments)
	this.books = 'Books'
}
// 继承
extend(SubCls, SuperCls)
// 加强
SubCls.prototype.getBooks = function() {
	return this.books
}

var subCls = new SubCls()
console.log(subCls.getName()) // Name
console.log(subCls.getBooks()) // Books

上面函数首先用一个空函数去引用父类的原型属性,防止构造过程中报错;同时将父类保存在子类中,子类可以在实例化的时候去继承父类属性。

原型式继承

原型式继承和类式继承最大的区别就是,原型式继承的对象就是一个字面量对象,它也是得益于原型链才得以实现。

下面是实现原型式继承的 clone 函数

function clone(superClass) {
	// 因为只有函数才有 prototype 属性
	var F = function() {}
	// 将父类设置为 F 的原型对象
	F.prototype = superClass
	return new F()
}

// 定义父类
var Person = {
	name: 'SuperClass',
	getName: function() {
		return this.name
	}
}

// 继承
var Author = clone(Person)
// 加强
Author.books = ['无']
Author.getBooks = function() {
	return this.books
}

var author = clone(Author)
console.log(author.getName()) // SuperClass
console.log(author.getBooks()) // ['无']

原型式继承相对于类式继承简洁了不少,首先定义一个类时,不需要通过构造函数;其次,子类继承时也不用定义构造函数,直接通过一个 clone 函数就能实现继承。

不过,原型式继承有一个最大的问题,就是属性读写不对等,例如:

function clone(superClass) {
	// 因为只有函数才有 prototype 属性
	var F = function() {}
	// 将父类设置为 F 的原型对象
	F.prototype = superClass
	return new F()
}

// 定义父类
var Person = {
	name: 'SuperClass',
	getName: function() {
		return this.name
	}
}

// 继承
var Author = clone(Person)
// 加强
Author.books = ['无']
Author.getBooks = function() {
	return this.books
}

var author_1 = clone(Author)
var author_2 = clone(Author)

console.log(author_1.getName()) // SuperClass
console.log(author_2.getName()) // SuperClass
author_1.name = 'NewName'
console.log(author_1.getName()) // NewName
console.log(author_2.getName()) // SuperClass

console.log(author_1.getBooks()) // ['无']
console.log(author_2.getBooks()) // ['无']
author_1.books.push('第一本')
console.log(author_1.getBooks()) // ["无", "第一本"]
console.log(author_2.getBooks()) // ["无", "第一本"]

author_1.books = ['第二本']
console.log(author_1.getBooks()) // ["第二本"]
console.log(author_2.getBooks()) // ["无", "第一本"]

对于继承而来的 name 属性,我们直接改写相当于重新定义了子类的 name 属性,它是基本类型,貌似并没有什么影响。但是如果我们改写引用类型的 books 属性,你会发现所有子类都受到了影响。

这是因为通过原型式继承而来的只是对象的一个引用,比不是完全的一个副本,当你更改引用对象的属性时,所有引用这个对象的子类都会受影响。

一个解决方法就是当你更改引用对象的属性时,需要重新定义这个对象引用,如上述代码一样。

原型式继承的一个优点就是节省内存,因为继承而来的属性都只有唯一一份。

掺元类

掺元类也是实现代码复用的一个方式,它不用去考虑继承,它是通过扩充的方式来实现代码复用。

具体做法就是先定义一个包含各种公用属性的类,然后通过属性复制的方式扩充其他类。

function mix(receivingCls, givingCls) {
	if (givingCls.prototype !== undefined && givingCls.prototype !== null) {
		givingCls = givingCls.prototype
	}

	for (var attr in givingCls) {
		if (!receivingCls[attr]) {
			receivingCls[attr] = givingCls[attr]
		}
	}
}

// 给予类
var Person = {
	name: 'SuperClass',
	getName: function() {
		return this.name
	}
}

// 受类
var Author = {
	books: ['第一本'],
	getBooks: function() {
		return this.books
	}
}

// 掺元
mix(Author, Person)

console.log(Author.getName()) // SuperClass