面向对象编程 (OOP) 是一种基本的编程范式,几乎每个开发人员都在其职业生涯的某个阶段使用过。OOP 是用于软件开发的最流行的编程范例,并且在大多数程序员的教育生涯中被作为标准编码方式教授。 另一种流行的编程范式是函数式编程,但我们现在不讨论它。
今天,我们将分解使程序面向对象的基础知识,以便您可以开始在您的算法、项目和面试中使用这种范式。
现在,让我们深入了解这些 OOP 概念和教程!
以下是将涵盖的内容:
- 什么是面向对象编程?
- OOP 的构建块
- OOP的四大原则
- 接下来要学什么
什么是面向对象编程?
面向对象编程 (OOP) 是计算机科学中的一种编程范式,它依赖于类和对象的概念。它用于将软件程序构建为简单、可重用的代码蓝图(通常称为类),用于创建对象的各个实例。有许多面向对象的编程语言,包括 JavaScript、C++ 、Java和Python。
OOP 语言不一定限于面向对象的编程范例。某些语言,例如 JavaScript、Python 和 PHP,都同时支持面向过程和面向对象的编程风格。
类是创建更具体 、 具体对象的抽象蓝图。类通常表示广泛的类别,例如Car
或Dog
共享属性。这些类定义此类型的实例将具有哪些属性,例如color
,但不定义特定对象的这些属性的值。
类还可以包含称为方法的函数,这些函数仅适用于该类型的对象。
这些函数在类中定义,并执行一些对特定对象类型有帮助的操作。
例如,我们的
Car
类可能有一个repaint
方法可以改变color
我们汽车的属性。
这个函数只对 type 的对象有帮助Car
,所以我们在Car
类中声明它,从而使它成为一个方法。
类模板用作创建单个对象的蓝图。这些代表抽象类的具体示例,例如myCar
或goldenRetriever
。每个对象都可以具有类中定义的属性的唯一值。
例如,假设我们创建了一个类 ,
Car
以包含汽车必须具有的所有属性color
、brand
和model
。然后我们创建一个Car
类型对象的实例,myCar
来代表我的特定汽车。然后我们可以设置类中定义的属性的值来描述我的车,而不影响其他对象或类模板。
然后我们可以重用这个类来表示任意数量的汽车。
OOP 对软件工程的好处
- OOP 将复杂事物建模为可重现的简单结构
- 可重用,OOP 对象可以跨程序使用
- 多态性允许类特定的行为
- 更容易调试,类通常包含所有适用的信息
- 通过封装安全地保护敏感信息
如何构造 OOP 程序
让我们以一个现实世界的问题为例,从概念上设计一个 OOP 软件程序。
想象一下,经营一个有数百只宠物的宠物托管营地,您可以在其中跟踪每只宠物的名字、年龄和参加的天数。
您将如何设计简单、可重复使用的软件来为狗建模?
如果有数百只狗,为每只狗编写唯一的条目将是低效的,因为您会编写大量冗余代码。下面我们看看对象rufus
和可能是什么样子fluffy
。
``` //Object of one individual dog var rufus = { name: "Rufus", birthday: "2/1/2017", age: function() { return Date.now() - this.birthday; }, attendance: 0 }
//Object of second individual dog var fluffy = { name: "Fluffy", birthday: "1/12/2019", age: function() { return Date.now() - this.birthday; }, attendance: 0 } ```
正如您在上面看到的,两个对象之间有很多重复的代码。该age()
功能出现在每个对象中。因为我们想要每只狗的相同信息,所以我们可以使用对象和类来代替。
将相关信息组合在一起形成一个类结构可以使代码更短并且更易于维护。
在看狗的例子中,程序员可以这样考虑组织 OOP:
- 为所有的狗创建一个类, 作为所有狗都会有的信息和行为(方法)的蓝图,无论类型。这也称为父类。
- *在主蓝图下创建子类来表示狗的不同子类别。* 这些也称为子类。
- *向子类添加独特的属性和行为以表示差异*
- *从代表该子组中的狗的子类创建对象*
下图展示了如何通过将相关数据和行为组合在一起形成一个简单模板,然后为专用数据和行为创建子组来设计 OOP 程序。
该类Dog
是一个通用模板,仅包含所有狗共有的数据结构和行为作为属性。
然后我们创建两个子类Dog
,HerdingDog
和TrackingDog
。Dog
它们具有( )的遗传行为bark()
,但也具有该亚型狗独有的行为。
最后,我们创建类型的对象HerdingDog
来表示个体狗Fluffy
和Maisel
。
Rufus
我们还可以创建适合大类 ofDog
但不适合HerdingDog
or 的对象TrackingDog
。
OOP 的构建块
接下来,我们将更深入地了解上面使用的 OOP 程序的每个基本构建块:
- 班级
- 对象
- 方法
- 属性
类
简而言之,类本质上是用户定义的数据类型。类是我们为方法和属性的结构创建蓝图的地方。从此蓝图中实例化各个对象。
类包含属性字段和行为方法。在我们的Dog
类示例中,属性包括name
& birthday
,而方法包括bark()
和updateAttendance()
。
下面是演示如何Dog
使用 JavaScript 语言编写类的代码片段。
``` class Dog { constructor(name, birthday) { this.name = name; this.birthday = birthday; }
//Declare private variables
_attendance = 0;
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return Date.now() - this.birthday;
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
} ```
请记住,该类是用于为狗建模的模板,并且对象是从表示单个现实世界项目的类中实例化的。
对象
毫无疑问,对象是 OOP 的重要组成部分!对象是使用特定数据创建的类的实例。例如,在下面的代码片段中,Rufus
是类的一个实例Dog
。
``` class Dog { constructor(name, birthday) { this.name = name; this.birthday = birthday; }
//Declare private variables
_attendance = 0;
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return Date.now() - this.birthday;
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
}
//instantiate a new object of the Dog class, and individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017"); ```
Dog
当调用新类时:
- 创建一个名为的新对象
rufus
- 构造函数运行
name
&birthday
arguments,并赋值
编程词汇:
在 JavaScript 中,对象是一种变量。这可能会引起混淆,因为在 JavaScript 中可以在没有类模板的情况下声明对象,如开头所示。
对象具有状态和行为。对象的状态由数据定义:例如姓名、生日和您想要存储的有关狗的其他信息。行为是对象可以采取的方法。
属性
属性是存储的信息。属性在模板中定义Class
。当对象被实例化时,单个对象包含存储在属性字段中的数据。
对象的状态由对象属性字段中的数据定义。例如,小狗和狗在宠物营中可能会受到不同的对待。生日可以定义对象的状态,并允许软件以不同方式处理不同年龄的狗。
方法
方法代表行为。方法执行动作;方法可能会返回有关对象的信息或更新对象的数据。该方法的代码在类定义中定义。
当实例化单个对象时,这些对象可以调用类中定义的方法。在下面的代码片段中,bark
方法是在Dog
类中定义的,bark()
方法是在Rufus
对象上调用的。
``` class Dog { //Declare protected (private) fields _attendance = 0;
constructor(name, birthday) {
this.namee = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
} ```
方法经常修改、更新或删除数据。方法不必更新数据。例如,该bark()
方法不会更新任何数据,因为吠叫不会修改Dog
类的任何属性:name
或birthday
。
该方法增加了参加宠物托管营的updateAttendance()
一天。Dog
出勤属性对于在月底为所有者开具账单很重要。
方法是程序员提高可重用性并将功能封装在对象中的方式。这种可重用性在调试时是一个很大的好处。如果有错误,只有一个地方可以找到并修复它,而不是很多。
中的下划线_attendance
表示该变量受保护,不应直接修改。方法updateAttendance()
变了_attendance
。
OOP的四大原则
面向对象编程的四大支柱是:
- 继承: 子类继承父类的数据和行为
- 封装: 在对象中包含信息,只暴露选定的信息
- 抽象: 仅公开用于访问对象的高级公共方法
- 多态性: 许多方法可以完成相同的任务
继承
继承允许类继承其他类的特性。换句话说,父类将属性和行为扩展到子类。继承支持可重用性。
如果在父类中定义了基本属性和行为,则可以创建子类,扩展父类的功能并添加额外的属性和行为。
例如,牧羊犬具有独特的放牧能力。也就是说,所有的牧羊犬都是狗,但并不是所有的狗都是牧羊犬。HerdingDog
我们通过从父类创建子类Dog
,然后添加独特的herd()
行为来表示这种差异。
继承的好处是程序可以创建一个通用的父类,然后根据需要创建更具体的子类。Dog
这简化了编程,因为无需多次重新创建类的结构,子类会自动访问其父类中的功能。
在下面的代码片段中,子类继承了父类的HerdingDog
方法,子类又增加了一个方法,。bark``Dog``herd()
``` //Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0;
constructor(namee, birthday) {
this.name = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
}
//Child class HerdingDog, inherits from parent Dog class HerdingDog extends Dog { constructor(name, birthday) { super(name); super(birthday); }
herd() {
//additional method for HerdingDog child class
return console.log("Stay together!")
}
} ```
请注意,该类HerdingDog
没有该bark()
方法的副本。它继承bark()
父Dog
类中定义的方法。
当代码调用fluffy.bark()
方法时,该bark()
方法沿着子类链向上到父类,以找到bark
定义该方法的位置。
``` //Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0;
constructor(namee, birthday) {
this.name = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
}
//Child class HerdingDog, inherits from parent Dog class HerdingDog extends Dog { constructor(name, birthday) { super(name); super(birthday); }
herd() {
//additional method for HerdingDog child class
return console.log("Stay together!")
}
}
//instantiate a new HerdingDog object const fluffy = new HerdingDog("Fluffy", "1/12/2019"); fluffy.bark(); ```
注意: 父类也称为超类或基类。子类也可以称为子类、派生类或扩展类。
在 JavaScript 中,继承也称为原型设计。原型对象是另一个对象继承属性和行为的模板。可以有多个原型对象模板,创建一个原型链。
这与父/子继承的概念相同。
继承是从父母到孩子。在我们的示例中,所有三只狗都会吠叫,但只有 Maisel 和 Fluffy 会放牧。
该herd()
方法是在子类中定义的,因此从该类实例化的HerdingDog
两个对象Maisel
和都可以访问该方法。Fluffy``HerdingDog``herd()
Rufus是父类实例化的对象Dog
,所以Rufus只能访问方法bark()
。
封装
封装意味着将所有重要信息包含在一个对象中,并且只将选定的信息暴露给外界。属性和行为由类模板内的代码定义。
然后,当从类实例化一个对象时,数据和方法被封装在该对象中。封装将内部软件代码实现隐藏在一个类内部,将内部对象的内部数据隐藏起来。
封装需要将一些字段定义为私有的,一些定义为公共的。
- 私有/内部接口: 可从同一类的其他方法访问的方法和属性。
- 公共/外部接口: 可从类外部访问的方法和属性。
我们用汽车来比喻封装。汽车与外界共享的信息,使用信号灯指示转弯,是公共接口。相比之下,发动机隐藏在引擎盖下。
这是一个私有的内部接口。当您在路上开车时,其他司机需要信息来做出决定,例如您是左转还是右转。但是,暴露内部的私人数据(例如发动机温度)会使其他驾驶员感到困惑。
封装增加了安全性。属性和方法可以设置为私有的,这样就不能在类外访问。为了获取有关对象中数据的信息,公共方法和属性用于访问或更新数据。
这增加了一层安全性,开发人员通过类定义中的公共方法公开该数据来选择可以在对象上看到哪些数据。
在类中,大多数编程语言都有公共、受保护和私有部分。公共部分是从外部世界或程序中的其他类访问的方法的有限选择。Protected 只能被子类访问。
私有代码只能从该类中访问。回到我们的狗/主人的例子,封装是理想的,这样主人就无法访问关于其他人的狗的私人信息。
注意: JavaScript 具有私有和受保护的属性和方法。
_
受保护的字段以;为前缀 私有字段以 . 为前缀#
。受保护的字段是继承的。私人的不是。
``` //Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0;
constructor(namee, birthday) {
this.name = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
}
//instantiate a new instance of Dog class, an individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017"); //use getter method to calculate Rufus' age rufus.getAge(); ```
考虑getAge()
我们示例代码中的方法,计算细节隐藏在Dog
类内部。该rufus
对象使用该getAge()
方法计算 Rufus 的年龄。
封装和更新数据: 由于方法也可以更新对象的数据,开发人员可以通过公共方法控制哪些值可以更改。
这使我们能够隐藏不应因网络钓鱼而更改的重要信息,以及其他开发人员错误更改重要数据的可能性更大的情况。
封装增加了代码的安全性,并使与外部开发人员的协作变得更加容易。当您编程与外部公司共享信息时,您不希望公开类的模板或私有数据,因为您的公司拥有该知识产权。
相反,开发人员创建允许其他开发人员调用对象方法的公共方法。理想情况下,这些公共方法带有供外部开发人员使用的文档。
封装的好处总结如下:
- 增加安全性: 只有公共方法和属性可以从外部访问
- 防止常见错误: 只有公共字段和方法是可访问的,因此开发人员不会意外更改危险的内容
- 保护IP: 代码隐藏在类中;外部开发人员只能访问公共方法
- 可支持: 大多数代码都经过更新和改进
- 隐藏复杂性: 没有人能看到物体幕后的东西!
抽象
抽象是封装的扩展,它使用包含数据和代码的类和对象来向用户隐藏程序的内部细节。这是通过在用户和更复杂的源代码之间创建一个抽象层来实现的,这有助于保护存储在源代码中的敏感信息。
抽象
- 降低复杂性并提高代码可读性
- 促进代码重用和组织
- 数据隐藏通过向用户隐藏敏感细节来提高数据安全性
- 通过抽象掉低级细节来提高生产力
抽象也可以用汽车来解释。想一想司机如何仅使用汽车的仪表板来操作车辆。
驾驶员使用汽车的方向盘、加速器和制动踏板来控制车辆。驾驶员不必担心发动机如何工作或每个动作使用哪些零件。这是一种抽象——只有驾驶员使用汽车所必需的重要方面是可见的。
同样,数据抽象允许开发人员处理复杂的信息,而不必担心其内部运作。这样,有助于提高代码质量和可读性。
抽象也起着重要的安全作用。通过仅显示选定的数据片段并仅允许通过类访问数据和通过方法修改数据,我们可以保护数据免于暴露。继续以汽车为例,您在开车时不希望打开油箱。
抽象的好处总结如下:
- 简单的高级用户界面
- 隐藏复杂代码
- 安全
- 更轻松的软件维护
- 代码更新很少改变抽象
多态性
多态性意味着设计对象以共享行为。使用继承,对象可以用特定的子行为覆盖共享的父行为。多态允许同一个方法以两种方式执行不同的行为:方法覆盖和方法重载。
方法覆盖
运行时多态性使用方法覆盖。在方法覆盖中,子类的实现方式可能与其父类不同。在我们的狗示例中,我们可能想要给出TrackingDog
一种不同于一般狗类的特定类型的吠声。
bark()
方法覆盖可以在子类中创建一个方法来覆盖bark()
父Dog
类中的方法。
``` //Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0;
constructor(namee, birthday) {
this.name = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
}
//Child class TrackingDog, inherits from parent class TrackingDog extends Dog { constructor(name, birthday) super(name); super(birthday); }
track() {
//additional method for TrackingDog child class
return console.log("Searching...")
}
bark() {
return console.log("Found it!");
}
//instantiate a new TrackingDog object const duke = new TrackingDog("Duke", "1/12/2019"); duke.bark(); //returns "Found it!" ```
方法重载
编译时多态性使用方法重载。方法或函数可能具有相同的名称,但传递给方法调用的参数数量不同。根据传入的参数数量,可能会出现不同的结果。
``` //Parent class Dog class Dog{ //Declare protected (private) fields _attendance = 0;
constructor(namee, birthday) {
this.name = name;
this.birthday = birthday;
}
getAge() {
//Getter
return this.calcAge();
}
calcAge() {
//calculate age using today's date and birthday
return this.calcAge();
}
bark() {
return console.log("Woof!");
}
updateAttendance() {
//add a day to the dog's attendance days at the petsitters
this._attendance++;
}
updateAttendance(x) {
//adds multiple to the dog's attendance days at the petsitters
this._attendance = this._attendance + x;
}
}
//instantiate a new instance of Dog class, an individual dog named Rufus const rufus = new Dog("Rufus", "2/1/2017"); rufus.updateAttendance(); //attendance = 1 rufus.updateAttendance(4); // attendance = 5 ```
在此代码示例中,如果没有参数传递到updateAttendance()
方法中。一天被添加到计数。如果传入一个参数updateAttendance(4)
,则传入x
参数4 updateAttendance(x)
,计数加4天。
多态的好处是:
- 不同类型的对象可以通过同一个接口传递
- 方法覆盖
- 方法重载
结论
面向对象编程需要在开始编码之前考虑程序的结构并规划出面向对象的设计。计算机编程中的 OOP 侧重于如何将需求分解为简单的、可重用的类,这些类可用于绘制对象实例的蓝图。总体而言,实施 OOP 可以实现更好的数据结构和可重用性,从长远来看可以节省时间。