继承是每个面向对象编程语言都会讨论的话题,只不过在 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