前端知识点

74 阅读6分钟

对象继承


常见的对象继承方法有:

对象的继承是面向对象编程中的一个核心概念,它指的是一个对象(子类或派生类)可以获取另一个对象(父类或基类)的属性和方法。通过继承,子类可以重用父类的代码,并且可以在此基础上添加或覆盖父类的属性和方法,从而实现代码的复用和扩展。

具体来说,继承有以下几个主要特点:

代码重用:子类可以直接继承父类的属性和方法,从而避免了在子类中重新编写与父类相同的代码。 扩展性:子类可以在继承父类的基础上,添加自己的属性和方法,或者覆盖父类的某些方法,以实现特定的功能。 层次结构:通过继承,可以创建出具有层次结构的类体系,使得类的设计更加清晰和有序。

原型链继承

原型继承的实质就是对原型修改效果的传递,基于原型链的特性:访问属性时,如果子对象没有该属性,则访问其原型的属性类。实际上,所有继承方式的本质都是原型链。

原型链继承就是通过构造函数的方式,将父类的实例赋值给子类的显示原型(prototype)上,以此类推,每一层都可以继承上一层的属性。

function Parent() {}
Parent.prototype.package = ['书']

function Children() {}
// 继承了 Parent
Children.prototype = new Parent()

var instance1 = new Children()
var instance2 = new Children()
instance1.package.push('笔')

console.log(instance1.package) // ['书', '笔']
console.log(instance2.package) // ['书', '笔']
console.log(instance1.package === instance2.package) // true 

在上述例子中,Children.prototype 是 Parent 函数的实例,继承Parent.prototype上的属性。当实例instance1向数组中添加值得时候,发生了浅拷贝,所以instance2创建后,继承了数组添加后的结果。

原型链继承的问题:

  1. 通过原型继承的时候,原型上的所有属性都是共享的,如果这个属性是引用类型的值,那么中途继承的实例一旦将属性修改,修改后的值也会在新继承的实例上获取到。
  2. 子类型在实例化时,无法在不影响所有对象实例的情况下,给父类型的构造函数传参。

借用构造函数继承

基本思路:在子类构造函数中调用父类构造函数。

使用apply()和call()方法以新创建的对象为上下文执行构造函数。

function Parent (name) {
  this.name = name
}

function Children () {
  // 继承了 Parent,同时传递了参数
  Parent.call(this, 'sunshine')
  // this.name = 'sunshine'

  // 实例属性
  this.age = 25
}

var instance = new Children()
console.log(instance) // { age: 25, name: "sunshine" }

Prent接收参数name然后给它赋值一个属性。在Children构造函数中调用Parent构造函数时传入了这个参数,会在Children的实例上定义name属性。为确保Parent函数不会覆盖Children定义的属性,所以在调用父函数再给子函数添加了age属性。

借用构造函数的问题:

  1. 必须在构造函数中定义方法,函数不能重用;
  2. 子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

对象继承

对象继承也叫原型式继承,不自定义类型也可以通过原型实现对象之间的信息共享。

function object(o) {
  // 创建一个临时构造函数
  function F () {}
  // 将传入的对象赋值给这个构造函数的原型
  F.prototype = o
  // 返回这个临时类型的一个实例
  return new F()
}

var parent = {
  name: '父亲',
  getName: function() {
    console.log(this.name)
  }
}

var children = object(parent)

parent.getName() // "父亲"
children.getName() // "儿子"

在ES5中增加Object.create方法将原型式继承的概念规范化了。该方法接受两个参数:第一个参数为新对象的prototype,第二个参数描述了新对象的属性。

var parent = {
  name: '父亲',
  getName: function() {
    console.log(this.name)
  }
}

var children = Object.create(parent, {
  name: { value: '儿子' }
})

parent.getName() // "父亲"
children.getName() // "儿子"

综上所述,对象继承非常不适合单独创建构造函数,但是仍然需要在对象之间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

寄生式继承

与对象继承最接近的一种继承方式是寄生式继承。寄生式继承的思路:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。实际上,寄生式继承就是将创建对象的过程做了一个封装。

function inherit (obj) {
  var cloneObj = Object.create(obj)
  cloneObj.getName = function() {
    console.log(this.name)
  }
  return cloneObj
}

var parent = {
  name: 'sunshine'
}

var children = inherit(parent)
children.getName() // 'sunshine'

寄生式继承的问题:

通过寄生式继承给对象添加函数会导致函数难以重用,与借用构造函数模式类似。

组合式继承

组合继承的思路:使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function Parent(name) {
  this.name = name
  this.package = ['书']
}

Parent.prototype.getName = function () {
  console.log(this.name)
}

function Children (name, age) {
  // 继承属性
  Parent.call(this, name) // 第2次调用Parent

  this.age = age
}

// 继承方法
Children.prototype = new Parent() // 第1次调用Parent
Children.prototype.getAge = function () {
  console.log(this.age)
}

// 使用
var instance1 = new Children('sunshine', 20)
var instance2 = new Children('colorful', 30)
instance1.package.push('笔')

console.log(instance1.package)  //[ '书', '笔' ]
instance1.getName()    //sunshine
instance1.getAge()    //20

console.log(instance2.package)     //[ '书' ]
instance2.getName()   //colorful
instance2.getAge()   //30

console.log(instance1 instanceof Parent) // true
console.log(Parent.prototype.isPrototypeOf(instance1)) // true

组合继承弥补了原型链和借用构造函数的不足,也保留了 instanceof 操作符和 isPrototypeOf() 方法的识别能力。

组合继承的问题:

效率问题:父类构造函数始终会被调用2次,一次是在创建子类型时调用,另一次是在子类构造函数中调用。这样就会导致子类和实例上都有相同的属性。

寄生式组合继承

寄生式组合继承通借用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本,即:使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function inherit(children, parent) {
	children.prototype = Object.create(parent.prototype)
  children.prototype.constructor = children
  children.__proto__ = parent
}

function Parent(name) {
  this.name = name
  this.package = ['书']
}

Parent.prototype.getName = function () {
  console.log(this.name)
}

function Children (name, age) {
  // 继承属性
  Parent.call(this, name)

  this.age = age
}

inherit(Children, Parent)
Children.prototype.getAge = function () {
  console.log(this.age)
}

// 使用
var instance1 = new Children('sunshine', 20)
instance1.getName()  //sunshine
instance1.getAge()  //20

这里的 inherit 函数实现了寄生式组合的核心逻辑。这个函数接收两个参数:子构造函数和父构造函数。

  1. 创建父类原型的一个副本
  2. 给返回 prototype 对象设置 constructor 属性,解决由于重写原型导致的 constructor 丢失问题
  3. 将创建的对象赋值给子类型的原型

寄生式组合继承可以算是引用类型继承的最佳模式。但是它还有一些不足之处,就是没有对静态属性的继承。

内置对象继承 我们知道,浏览器的内置对象有很多种,其中包括 Date,Number,String等,对于这种内置的构造器而言,是不能用上面的方式继承的。以Date为例会出现错误的原因是日期对象只能通过 Date 构造函数来实例化生成。

类继承 在ES6时代,我们可以使用 class extends 进行继承。

class DateConstructor extends Date {
  constructor () {
    super()
    this.foo = 'bar'
  }
  getMyTime () {
    return this.getTime()
  }
}

var date = new DateConstructor()
console.log(date.getMyTime())