集齐 JavaScript 中的继承方式

JavaScript 中的继承是以原型为基础的,所以首先要弄明白原型,在上一篇文章「JavaScript 中的原型原来是这样的」里应该会有所获。

继承是什么

在传统面向对象语言( c++, c#, java… )中,继承是:使用已存在的类作为基础建立新的类的技术。我们把已存在的类称之为父类,新的类称之为子类。继承可以使得子类具有父类的各种属性和方法,因此可以避免写重复代码提高开发效率。

而在 JavaScript 中是没有这一概念的,但万物皆对象(除原始类型),我们用构造函数的原型对象代表父类对象,构造函数作为子类。通过构造函数创建出来的实例对象(子类实例)可以通过 __proto__ 访问到父类中的方法和属性,同样实现继承机制。

继承

继承的方式

由于 JavaScript 的灵活,所以实现继承的方式也是有很多种的,我们先从简单的开始。

原型链继承

原型链继承顾名思义就是使用原型链实现继承,核心一句话:用父类的实例来充当子类的原型对象

我们先看一段代码:

function Animal(name) {
this.name = name
// 动态类型模式 利用原型共享方法
if(this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}

function Dog(age) {
this.age = age
}

// 用父类的实例来充当子类的原型对象
Dog.prototype = new Animal('动物')

var d = new Dog(3)
console.log(d.age) // 3
console.log(d.name) // '动物'
d.eat() // '吃'

在浏览器控制台里:

画图分析一波:

原型链继承

但是在上面的例子里,我们不能在实例 Dog 子类时传递 name 参数的值。不能更改父类的 name 属性值。如果加一条 d.name = '狗'那么子类属性 name 会屏蔽父类属性 name

所以这种方式能够很好继承父类原型中的方法(方法一般不会去修改它),但是不能很好继承父类的属性(属性修改会变成给子类实例赋新的属性值)。

总结缺点:子类在构建实例的时候不能向父类传递参数,导致不能很好的继承父类的属性。

借用构造函数继承

在子类构造函数中借用父类的构造方法(使用 call()apply())

function Animal(name) {
this.name = name
// 动态类型模式 利用原型共享方法
if(this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}

function Dog(name,age) {
// 借用父类的构造函数
Animal.call(this,name)

/* 借用构造函数Animal 相当于执行了以下操作:
this.name = name
Animal.prototype.eat = function() {
console.log('吃')
}
*/

this.age = age
}

var d = new Dog('哈士奇', 2)

console.log(d.name) // '哈士奇'
console.log(d.age) // 2
d.eat() // Uncaught TypeError: d.eat is not a function

浏览器控制台中:

这种方式与上种方式恰好相反。子类不会继承父类原型中的任何属性和方法(即父类的原型不会被共享),但是它可以在构建实例时向父类传参,将父类本身的属性带到子类实例本身上。

组合继承

原型链继承 + 借用构造函数继承双剑合璧,是 JavaScript 中最常用的继承方式。

function Animal(name) {
this.name = name
// 动态类型模式 利用原型共享方法
if(this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}

function Dog(name,age) {
// 1.借用父类的构造函数
Animal.call(this,name) // 第一次调用 Animal
this.age = age
}

// 2.用父类实例充当子类原型
Dog.prototype = new Animal('动物') // 第二次调用 Animal
Dog.prototype.constructor = Dog // 手动指明构造函数

var d = new Dog('哈士奇', 2)

console.log(d.name) // '哈士奇'
console.log(d.age) // 2
d.eat() // '吃'

浏览器控制台中:

这种方法融合了上两种的优点,但还是有缺点的,由于调用了两次 Animal() 函数,导致对象 d 本身和原型上都有 name 属性,原型上的 name 属性是多余的,这样既浪费性能又浪费内存。

原型式继承

ES5Object.creat() 的模拟实现,将传入的对象,作为新创建的对象的原型,并把这个对象返回。

function createObj(o) {
function F() {}
F.prototype = o
return new F()
}

// 父类对象
var animal = {
name:'动物',
eat: function() {
console.log('吃')
}
}

// 子类对象
var dog = createObj(animal)
// 等同于 var dog = Object.create(animal)
dog.age = 1

console.log(dog.name) // '动物'
console.log(do.age) // 1
dog.eat() // '吃'

浏览器控制台中:

这种方式和原型链继承的优缺点相同,不能向父类传递参数。

寄生式继承

利用原型式继承 + 工厂模式封装子类创建的过程,在创建过程中增强了子类对象(添加了属性方法)。

function createDog(o,age) {
// Object.create(o) <===> createObj(o) 原型式继承
var dog = Object.create(o)
dog.age = age
dog.bark = function() {
console.log('┗|`O′|┛ 嗷~~')
}
return dog
}

// 父类对象
var animal = {
name:'动物',
eat: function() {
console.log('吃')
}
}

// 子类对象
var dog = createDog(animal,2)

console.log(dog.name) // '动物'
console.log(dog.age) // 2
dog.bark() // '┗|`O′|┛ 嗷~~'
dog.eat() // '吃'

浏览器控制台中:

这种方式,每次创建一次对象,就会创建一边 bark() 方法。

寄生组合式继承

借用构造函数继承 + 寄生式继承,这种方式是解决了组合继承的问题(调用了两次父类的构造函数),它是最佳的继承方式。

function Animal(name) {
this.name = name
// 动态类型模式 利用原型共享方法
if(this.eat !== 'function') {
Animal.prototype.eat = function() {
console.log('吃')
}
}
}

function Dog(name,age) {
// 借用父类的构造函数
Animal.call(this,name)
this.age = age
}


/* 关键部分: 优化掉 new Animal(),
间接的让 Dog.prototype 访问到 Animal.prototype */

// 这里用 Object.create(原型, 构造器)取代寄生式继承 createDog(原型, 构造器),写起来更简单。
Dog.prototype = Object.create(Animal.prototype, {
constructor: {
value: Dog,
enumerable: false, // 枚举时不可见
writable: true, // 可被赋值运算改变
configurable: true //可以被改变并且从对应对象中删除。
}
})

var d = new Dog('哈士奇', 3)

console.log(d.name) // '哈士奇'
console.log(d.age) // '3'
d.eat() // '吃'

浏览器控制台中:

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承和寄生式继承。这个方法接收两个参数:一个用作新对象原型的对象和一个可选的为新对象定义额外属性的对象。

这种方式高效体现在它只调用了一次 Animal 构造函数,因此舍弃了在 Animal 上多余的不需要的属性。

class 语法糖

在 JavaScript 中并没有类的概念,但在 ES6 中,我们可以使用 class 写出类似传统面向对象语言的类的声明继承。但是本质上还是和以前没有多大区别,它只是一个语法糖,方便开发者书写。

构造函数 VS class 语法

class 继承

class Animal {
constructor(name) {
this.name = name
}
eat() {
console.log('吃')
}
}

class Dog extends Animal {
constructor(name,age) {
// 相当于之前继承中的 Animal.call(this, name)
super(name)

this.age = age
}
bark() {
console.log(this.name + ':嗷~~')
}
}

var d = new Dog('哈士奇',3)

console.log(d.name) // '哈士奇'
console.log(d.age) // '3'
d.eat() // '吃'
d.bark() // '哈士奇:嗷~~'

浏览器控制台中:

class 实现继承要注意两点:

  • 使用 extends 声明继承哪个父类
  • 在子类的构造函数中必须调用 super()

总结

  • 继承是子类拥有父类的一些属性和方法,减少重复代码的书写
  • 除了借用构造函数继承方式,其他继承方式都是基于原型的
  • class 是 ES6 中的语法糖,实现继承时要用 extends 声明继承的父类,子类构造函数中要写 super() 方法
© 2019 墨夜 All Rights Reserved.
Theme by hiero