JavaScript继承与原型链

原创
2020/08/06 18:40
阅读数 319

JavaScript 是一个面向对象的语言,但是 JavaScript 中的对象却不是真正的面向对象,因为 JavaScript 中的对象并没有继承的概念。

面向对象编程(Object-Oriented Programming,OOP)是一种基于“对象”概念的编程范式,“对象”可以包含数据和代码。数据采用字段的形式(通常称为属性或属性),代码采用过程的形式(通常称为方法)。

面向对象的特性

  • 封装性:具备封装性(Encapsulation)的面向对象程序设计隐藏了某一方法的具体执行步骤,取而代之的是通过消息传递机制发送消息给它。封装是通过限制只有特定类的对象可以访问这一特定类的成员,而它们通常利用接口实现消息的传入传出。
  • 继承性:继承性(Inheritance)是指,在某种情况下,一个类会有“子类”。子类比原本的类(称为父类)要更加具体化。
  • 多态性:多态(Polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。
  • 抽象性:抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。

封装

我们先来封装一个 Person 类:

// 封装Person类
class Person {
  constructor(name) {
    this.name = name
  }
  introduceSelf() {}
}

当其他部分的代码想要执行对象的某些操作时,可以借助对象向外部提供的接口完成操作,借此,对象保持了自身的内部状态不会被外部代码随意修改。也就是说,对象的内部状态保持了私有性,而外部代码只能通过对象所提供的接口访问和修改对象的内部状态,不能直接访问和修改对象的内部状态。保持对象内部状态的私有性、明确划分对象的公共接口和内部状态,这些特性称之为封装(encapsulation)。

封装的好处在于,当程序员需要修改一个对象的某个操作时,程序员只需要修改对象对应方法的内部实现即可,而不需要在所有代码中找出该方法的所有实现,并逐一修改。某种意义上来说,封装在对象内部和对象外部设立了一种特别的“防火墙”。

继承

我们定义 Professor 类和 Student 类继承 Person 类的特性。换句话说,Professor 类和 Student 类由 Person 类派生(derive)而来。

// Professor类继承Person类
class Professor extedns Person {
  constructor(name, teaches) {
    super(name)
    this.teaches = teaches
  }
  grade(paper) {}
  introduceSelf() {}
}

// Student类继承Person类
class Student extends Person {
  constructor(name, year) {
    super(name)
    this.year = year
  }
  introduceSelf() {}
}

在某种层级上,二者实际上是同种事物,他们能够具有相同的属性也是合理的。继承(Inheritance)可以帮助我们完成这一操作。

多态

我们在三个类中都定义了 introduceSelf() 方法来按各自的方式去做这件事。

// 在Professor类中,重写父类Person的introduceSelf方法
class Professor extedns Person {
  // ...省略
  introduceSelf() {}
}

// 在Student类中,重写父类Person的introduceSelf方法
class Student extends Person {
  // ...省略
  introduceSelf() {}
}

当一个方法拥有相同的函数名,但是在不同的类中可以具有不同的实现时,我们称这一特性为多态(polymorphism)。当一个方法在子类中替换了父类中的实现时,我们称之为子类重写/重载(override)了父类中的实现。

继承的方式

原型是 JavaScript 的一个强大且非常灵活的功能,使得重用代码和组合对象成为可能。特别是它们支持某种意义的继承。继承是面向对象的编程语言的一个特点,它让程序员表达这样的想法:系统中的一些对象是其他对象的更专门的版本。

原型编程是一种面向对象编程 的风格。在这种风格中,我们不会显式地定义类 ,而会通过向其他类的实例(对象)中添加属性和方法来创建类,甚至偶尔使用空对象创建类。简单来说,这种风格是在不定义 class 的情况下创建一个 object

基于原型的编程(Prototype-based programming)是一种面向对象的编程风格,其中行为重用(称为继承)是通过重用充当原型的现有对象的过程来执行的。该模型也可以称为原型、面向原型、无类或基于实例的编程。

先定义一个超类:

// 超类:Person类
function Person (name, age) {
  // 属性
  this.name = name
  this.age = age

  // 属性方法
  this.getName = function () {
    return this.name
  }
}

// 原型方法
Person.prototype.getAge = function () {
  return this.age
}

原型链继承

核心: 将父类的实例作为子类的原型

方法1:父类实例挂载到子类prototype

// 子类:Student类
function Student (name, age, grade) {
  this.grade = grade
}

Student.prototype = new Person()

// 实例
const s = new Student('jay', 40, 'A')

方法2:父类实例挂载到子类实例__proto__

值得注意的是,{ __proto__: ... } 语法与 obj.__proto__ 访问器不同:前者是标准且未被弃用的。

// 子类:Student类
function Student (name, age, grade) {
  this.grade = grade
}

// 实例
const s = new Student('jay', 40, 'A')
s.__proto__ = new Person()
  • 特点:
    • 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
    • 父类新增原型方法/原型属性,子类都能访问到
    • 简单,易于实现
  • 缺点
    • 如果要新增原型属性和方法,则必须放在 new 超类之后执行
    • 无法实现多继承
    • 来自原型对象的所有属性被所有实例共享
    • 创建子类实例时,无法向父类构造函数传参

借用构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

// 子类:Student类
function Student (name, age, grade) {
  Person.call(this, name, age)

  this.grade = grade
}

// 实例
const s = new Student('jay', 40, 'A')
  • 特点:
    • 解决了原型链继承子类实例共享父类引用属性的问题
    • 创建子类实例时,可以向父类传递参数
    • 可以实现多继承(call多个父类对象)
  • 缺点:
    • 实例并不是父类的实例,只是子类的实例
    • 只能继承父类的实例属性和方法,不能继承原型属性/方法
    • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

实例继承

核心:为父类实例添加新特性,作为子类实例返回

// 子类:Student类
function Student (name, age, grade) {
  const instance = new Person(name, age)

  this.grade = grade

  return instance
}

// 实例
const s = new Student('jay', 40, 'A')
  • 特点:
    • 不限制调用方式,不管是 new 子类还是子类,返回的对象具有相同的效果
  • 缺点:
    • 实例是父类的实例,不是子类的实例
    • 不支持多继承

拷贝继承

// 子类:Student类
function Student (name, age, grade) {
  const instance = new Person(name, age)

  for (let key in instance) {
    if (instance.hasOwnProperty(key)) {
      this[key] = instance[key]
    }
  }

  this.grade = grade
}

// 实例
const s = new Student('jay', 40, 'A')
  • 特点:
    • 支持多继承
  • 缺点:
    • 效率较低,内存占用高(因为要拷贝父类的属性)
    • 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

组合继承(经典继承)

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

// 子类:Student类
function Student (name, age, grade) {
  Person.call(this, name, age)

  this.grade = grade
}

Student.prototype = new Person()
Student.prototype.constructor = Student

// 实例
const s = new Student('jay', 40, 'A')
  • 特点:
    • 弥补了构造函数继承的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
    • 既是子类的实例,也是父类的实例
    • 不存在引用属性共享问题
    • 可传参
    • 函数可复用
  • 缺点:
    • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

原型式继承

方法1:创建函数

function create (o) {
  function F () { }

  F.prototype = o
  
  return new F()
}

const person = {
  name: 'jay',
  age: 40
}

const p = create(person)

方法2:Object.create

const student = {
  name: 'jay',
  age: 40
}

const s = Object.create(student)

两种方法本质上没差别,和原型链继承一样存在引用类型被共享的问题。

  • 特点:
    • 类似于复制一个对象,用函数来包装
  • 缺点:
    • 所有实例都会继承原型上的属性
    • 无法实现复用

寄生式继承

function create (o) {
  function F () { }

  F.prototype = o
  
  return new F()
}

function clone (o) {
	const obj = create(o)

	return obj
}

const person = {
	name: 'jay',
  age: 40
}

const s = clone(person)
  • 特点:
    • 写法简单,不需要单独创建构造函数
  • 缺点:
    • 和借用构造函数继承类似,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

寄生组合式继承

// 子类:Student类
function Student (name, age, grade) {
  Person.call(this, name, age)
  this.grade = grade
}

const F = function () {}
F.prototype = Person.prototype
Student.prototype = new F()
Student.prototype.constructor = Student
Student.__proto__ = Person

// 实例
const s = new Student('jay', 40, 'A')
  • 特点:
    • 堪称完美
  • 缺点:
    • 实现较为复杂

Class继承

class Person {
   constructor (name, age) {
    this.name = name
    this.age = age
  }

  static getName () {
    return this.name
  }

  getAge () {
    return this.age
  }
}

class Student extends Person {
  constructor (name, age, grade) {
    super(name, age)

    this.grade = grade
  }
}

// 实例
const s = new Student('jay', 40, 'A')

原型链

前面的章节中提到三个核心的概念:

  • 原型对象:每个函数对象都有一个原型对象prototype
  • 原型链:原型链很自然地实现了继承特性,每个对象都有一个私有属性__proto__指向创建该对象的构造函数的原型 prototype 的对象。
  • 构造函数:构造函数可以实现类的定义,包括定义类的方法。不过,原型也可以用于实现类的定义。每个原型对象都有一个 constructor 指向该原型对象所在的构造函数本身。

最后,我们来看一下两个继承类型代表的原型链情况,也就是 functionclass 原型链的差别。

寄生组合式继承

// 父类 P
function P () {}

// 子类 C
function C () {
	P.call(this)
}

const F = function () {}
F.prototype = P.prototype
C.prototype = new F()
C.prototype.constructor = C
C.__proto__ = P

// 实例
const f = new C()

Class继承

// 父类 P
class P {
	constructor () {}
}

// 子类 C
class C extends P {
	constructor () {
		super()
	}
}

// 实例
const f = new C()

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
1 收藏
1
分享
返回顶部
顶部