面向对象编程
面向对象编程(OOP)是一种编程范式,是许多编程语言(包括 Java 和 C++)的基础。本文将概述 OOP 的基本概念。我们将介绍三个主要概念:**类和实例**、**继承**和**封装**。目前,我们将描述这些概念,不特别参考 JavaScript,因此所有示例都使用伪代码给出。
注意:准确地说,此处描述的功能属于一种称为**基于类**或“经典”OOP 的特定 OOP 风格。当人们谈论 OOP 时,通常指的就是这种类型。
之后,在 JavaScript 中,我们将了解构造函数和原型链如何与这些 OOP 概念相关联,以及它们之间的区别。在下一篇文章中,我们将介绍 JavaScript 的一些其他功能,这些功能使实现面向对象程序变得更容易。
先决条件 | 了解 JavaScript 函数,熟悉 JavaScript 基础知识(参见第一步和构建块),以及 OOJS 基础知识(参见对象简介和对象原型)。 |
---|---|
目标 | 理解基于类的面向对象编程的基本概念。 |
面向对象编程是关于将系统建模为对象的集合,其中每个对象代表系统的某个特定方面。对象包含函数(或方法)和数据。对象为想要使用它的其他代码提供公共接口,但同时维护自己的私有内部状态;系统其他部分不必关心对象内部发生了什么。
类和实例
当我们在 OOP 中用对象来建模问题时,我们会创建抽象定义来表示我们希望在系统中拥有的对象类型。例如,如果我们正在对学校进行建模,我们可能希望拥有表示教授的对象。每个教授都有一些共同的属性:他们都有姓名和教授的科目。此外,每个教授都可以做某些事情:例如,他们都可以批改作业,并在学年开始时向学生介绍自己。
所以Professor
可以在我们的系统中成为一个类。类的定义列出了每个教授拥有的数据和方法。
在伪代码中,Professor
类可以这样写
class Professor properties name teaches methods grade(paper) introduceSelf()
这定义了一个具有以下属性的Professor
类:
- 两个数据属性:
name
和teaches
- 两个方法:
grade()
用于批改作业,introduceSelf()
用于自我介绍。
类本身不做任何事情:它是一种创建该类型具体对象的模板。我们创建的每个具体教授都称为Professor
类的实例。创建实例的过程由一个称为构造函数的特殊函数执行。我们向构造函数传递任何我们希望在新实例中初始化的内部状态的值。
通常,构造函数作为类定义的一部分写出,并且通常与类本身具有相同的名称
class Professor properties name teaches constructor Professor(name, teaches) methods grade(paper) introduceSelf()
此构造函数采用两个参数,因此我们可以在创建新的具体教授时初始化name
和teaches
属性。
现在我们有了构造函数,我们可以创建一些教授了。编程语言通常使用关键字new
来表示正在调用构造函数。
walsh = new Professor("Walsh", "Psychology");
lillian = new Professor("Lillian", "Poetry");
walsh.teaches; // 'Psychology'
walsh.introduceSelf(); // 'My name is Professor Walsh and I will be your Psychology professor.'
lillian.teaches; // 'Poetry'
lillian.introduceSelf(); // 'My name is Professor Lillian and I will be your Poetry professor.'
这创建了两个对象,它们都是Professor
类的实例。
继承
假设在我们的学校中,我们还想表示学生。与教授不同,学生不能批改作业,不教授特定科目,并且属于特定年级。
但是,学生确实有姓名,也可能希望自我介绍,因此我们可以这样写出学生类的定义
class Student properties name year constructor Student(name, year) methods introduceSelf()
如果我们能够表示学生和教授共享某些属性,或者更准确地说,表示他们在某种程度上是“相同类型的事物”,那就很有帮助了。继承让我们可以做到这一点。
我们首先观察到学生和教授都是人,人都具有姓名并希望自我介绍。我们可以通过定义一个新的类Person
来模拟这一点,在其中我们定义了所有人的共同属性。然后,Professor
和Student
都可以从Person
派生,并添加它们的额外属性
class Person properties name constructor Person(name) methods introduceSelf() class Professor : extends Person properties teaches constructor Professor(name, teaches) methods grade(paper) introduceSelf() class Student : extends Person properties year constructor Student(name, year) methods introduceSelf()
在这种情况下,我们会说Person
是Professor
和Student
的超类或父类。相反,Professor
和Student
是Person
的子类或子类。
您可能会注意到introduceSelf()
在所有三个类中都已定义。这样做的原因是,虽然所有人都希望自我介绍,但他们的介绍方式却有所不同
walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf(); // 'My name is Professor Walsh and I will be your Psychology professor.'
summers = new Student("Summers", 1);
summers.introduceSelf(); // 'My name is Summers and I'm in the first year.'
我们可能有一个针对不是学生或教授的人的introduceSelf()
的默认实现
pratt = new Person("Pratt");
pratt.introduceSelf(); // 'My name is Pratt.'
当方法具有相同的名称但在不同的类中具有不同的实现时,此功能称为多态性。当子类中的方法替换超类的实现时,我们说子类覆盖了超类中的版本。
封装
对象为想要使用它们的代码提供接口,但同时维护自己的内部状态。对象的内部状态保持私有,这意味着它只能被对象自己的方法访问,而不能被其他对象访问。保持对象的内部状态私有,并且通常在对象的公共接口和其私有内部状态之间进行清晰的划分,这称为封装。
这是一个有用的功能,因为它使程序员能够更改对象的内部实现,而无需查找和更新所有使用它的代码:它在该对象与系统其余部分之间创建了一种“防火墙”。
例如,假设学生如果在二年级或以上,则可以学习射箭。我们只需公开学生的year
属性,其他代码可以检查该属性以确定学生是否可以选修该课程
if (student.year > 1) {
// allow the student into the class
}
问题是,如果我们决定更改允许学生学习射箭的标准——例如,还要求父母或监护人同意——我们需要更新系统中执行此测试的每个位置。最好在Student
对象上有一个canStudyArchery()
方法,该方法在一个地方实现逻辑
class Student : extends Person properties year constructor Student(name, year) methods introduceSelf() canStudyArchery() { return this.year > 1 }
if (student.canStudyArchery()) {
// allow the student into the class
}
这样,如果我们想更改学习射箭的规则,我们只需要更新Student
类,并且所有使用它的代码仍然可以工作。
在许多 OOP 语言中,我们可以通过将某些属性标记为private
来阻止其他代码访问对象的内部状态。如果对象外部的代码尝试访问它们,这将生成错误
class Student : extends Person properties private year constructor Student(name, year) methods introduceSelf() canStudyArchery() { return this.year > 1 } student = new Student('Weber', 1) student.year // error: 'year' is a private property of Student
在不强制执行此类访问的语言中,程序员使用命名约定(例如,以下划线开头名称)来指示应将该属性视为私有。
OOP 和 JavaScript
在本文中,我们描述了基于类的面向对象编程的一些基本功能,这些功能在 Java 和 C++ 等语言中实现。
在前面两篇文章中,我们了解了两个核心 JavaScript 功能:构造函数和原型。这些功能当然与上面描述的一些 OOP 概念有一定关系。
- JavaScript 中的构造函数为我们提供了类似于类定义的东西,使我们能够在一个地方定义对象的“形状”,包括它包含的任何方法。但原型也可以在这里使用。例如,如果在构造函数的
prototype
属性上定义了一个方法,那么使用该构造函数创建的所有对象都通过其原型获得该方法,我们不需要在构造函数中定义它。 - 原型链似乎是实现继承的自然方式。例如,如果我们可以拥有一个原型为
Person
的Student
对象,那么它可以继承name
并覆盖introduceSelf()
。
但值得了解这些功能与上面描述的“经典”OOP 概念之间的区别。我们将在本文中重点介绍其中的一些。
首先,在基于类的 OOP 中,类和对象是两个独立的构造,并且对象始终作为类的实例创建。此外,定义类(类语法本身)的功能与实例化对象(构造函数)的功能之间存在区别。在 JavaScript 中,我们可以并且经常在没有任何单独的类定义的情况下创建对象,无论是使用函数还是对象文字。这使得处理对象比在经典 OOP 中轻量级得多。
其次,尽管原型链看起来像继承层次结构,并且在某些方面表现得像继承层次结构,但在其他方面却有所不同。当实例化子类时,会创建一个单个对象,该对象将子类中定义的属性与层次结构中更高级别定义的属性组合在一起。使用原型,层次结构的每个级别都由一个单独的对象表示,并且它们通过__proto__
属性链接在一起。原型链的行为更像委托而不是继承。委托是一种编程模式,其中一个对象在被要求执行任务时,可以自己执行任务或要求另一个对象(其**委托**)代表它执行任务。在许多方面,委托比继承是一种更灵活的对象组合方式(例如,可以在运行时更改或完全替换委托)。
也就是说,构造函数和原型可以用来在 JavaScript 中实现基于类的 OOP 模式。但是直接使用它们来实现诸如继承之类的功能非常棘手,因此 JavaScript 提供了额外的功能,这些功能构建在原型模型之上,更直接地映射到基于类的 OOP 的概念。这些额外功能是下一篇文章的主题。