面向对象编程 (OOP) 是许多编程语言(包括 Java 和 C++)的基本编程范例。在本文中,我们将概述 OOP 的基本概念。我们将描述三个主要概念:类和实例、继承和封装。
注意:确切地说,这里描述的功能是一种特定的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()
此构造函数采用两个参数,因此我们可以在创建新的具体教授时初始化 and 属性。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.'
Copy to Clipboard
这将创建两个对象,这两个对象都是该类的实例。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.'
Copy to Clipboard
对于不是学生或教授的人,我们可能默认实现 :introduceSelf()
pratt = new Person('Pratt')
pratt.introduceSelf() // 'My name is Pratt.'
Copy to Clipboard
此功能(当方法具有相同的名称但在不同的类中具有不同的实现时)称为多态性。当子类中的方法替换超类的实现时,我们说子类重写超类中的版本。
封装
对象为想要使用它们但维护自己的内部状态的其他代码提供接口。对象的内部状态保持私有,这意味着它只能通过对象自己的方法访问,而不能从其他对象访问。保持对象的内部状态是私有的,并且通常在它的公共接口和它的私有内部状态之间做出明确的划分,这称为封装。
这是一个有用的功能,因为它使程序员能够更改对象的内部实现,而不必查找和更新使用它的所有代码:它在此对象和系统的其余部分之间创建了一种防火墙。
例如,假设学生在第二年或以上时可以学习射箭。我们可以通过公开学生的属性来实现这一点,其他代码可以检查它以决定学生是否可以参加课程:year
if (student.year > 1) {
// allow the student into the class
}
Copy to Clipboard
问题是,如果我们决定改变允许学生学习射箭的标准 - 例如,通过要求父母或监护人给予许可 - 我们需要更新我们系统中执行此测试的每个地方。最好在对象上有一个方法,在一个地方实现逻辑:canStudyArchery()
Student
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
}
Copy to Clipboard
这样,如果我们想改变学习射箭的规则,我们只需要更新类,所有使用它的代码仍然有效。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
- 原型链似乎是实现继承的自然方式。例如,如果我们有一个对象,其原型是 ,那么它可以继承和覆盖 。
Student
Person
name
introduceSelf()
但值得了解这些功能与上述“经典”OOP概念之间的差异。我们将在这里重点介绍其中的几个。
首先,在基于类的 OOP 中,类和对象是两个独立的构造,对象始终作为类的实例创建。此外,用于定义类的功能(类语法本身)和用于实例化对象的功能(构造函数)之间也存在差异。在JavaScript中,我们可以并且经常创建没有任何单独类定义的对象,无论是使用函数还是对象文本。这可以使处理对象比在经典OOP中更轻量级。
其次,尽管原型链看起来像一个继承层次结构,并且在某些方面表现得像它,但在其他方面则不同。实例化子类时,将创建一个对象,该对象将子类中定义的属性与层次结构中进一步定义的属性组合在一起。使用原型设计,层次结构的每个级别都由一个单独的对象表示,并且它们通过属性链接在一起。原型链的行为不太像继承,而更像是委托。委派是一种编程模式,其中对象在被要求执行任务时,可以执行任务本身或要求另一个对象(其委托)代表它执行任务。在许多方面,委派是一种比继承更灵活的组合对象的方式(首先,可以在运行时更改或完全替换委托)。__proto__
也就是说,构造函数和原型可用于在JavaScript中实现基于类的OOP模式。但是直接使用它们来实现像继承这样的功能是很棘手的,所以JavaScript提供了额外的功能,层叠在原型模型之上,更直接地映射到基于类的OOP的概念。这些额外功能是下一篇文章的主题。
总结
本文描述了基于类的面向对象编程的基本功能,并简要介绍了 JavaScript 构造函数和原型如何与这些概念进行比较。