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
指向该原型对象所在的构造函数本身。
最后,我们来看一下两个继承类型代表的原型链情况,也就是 function
和 class
原型链的差别。
寄生组合式继承
// 父类 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()