轻松学习 JavaScript 面向对象编程
面向对象编程的核心思想是将程序分成更小的部分,并让每个部分负责管理自己的数据。
本文提供了一种全面而易于理解的面向对象编程(OOP)学习方法。其目标是理解基于类的面向对象编程的基本概念。
读完本文,您将了解以下概念:
-
对象
-
课程
-
抽象
-
遗产
-
多态性
-
封装
我们开始吧!
什么是面向对象编程?
面向对象编程(OOP)是一种编程方法,其中所有软件单元都是围绕对象而不是函数和逻辑组织的。
就像细胞构成生命的基本单位一样,对象构成面向对象编程的基本单位。
面向对象编程 (OOP) 提供了一种将应用程序的不同方面与对象隔离创建和管理的方法,并且可以独立地将它们连接起来。它还有助于代码重用,提供更简洁、更易于维护的代码,并消除冗余。
面向对象编程由四大支柱构成
-
封装
-
抽象
-
遗产
-
多态性
这些概念将在接下来的章节中详细介绍,在此之前,让我们先来探讨一下面向对象编程的重要性。
面向对象编程的重要性
面向对象编程的引入是为了消除过程式编程中遇到的缺陷。
过程式编程采用顺序执行的方式,并将数据和过程(函数)视为两个不同的实体。当你使用过程式编程编写应用程序时,你会发现函数会随着程序的开发分散在整个程序中。
程序性方法的缺点如下:
-
由于程序代码通常不可重用,因此如果应用程序的某个方面需要,您可能需要复制和粘贴代码行(重新创建代码)。
-
由于优先考虑的是流程而不是数据,因此数据会暴露给整个应用程序,这在某些数据敏感的情况下会造成问题。
引入面向对象编程(OOP)后,数据和功能被结合成一个称为对象的单一实体。
-
这样可以更轻松地独立创建和管理应用程序的不同方面,并将它们独立连接起来。
-
由于我们的应用程序是以单元(对象)为基础构建的,因此我们的代码组织良好、更加灵活且易于维护(避免了“意大利面条式代码”)。
理解面向对象编程中的对象
对象是面向对象编程的核心。对象可以代表一个具有数据和行为的现实实体。它是相关数据(属性)和/或功能(方法)的集合。
以现实生活中常见的物体——狗为例。狗具有某些特征:颜色、名字、体重等等。它还可以进行一些动作:吠叫、睡觉、吃饭等等。
在面向对象编程(OOP)术语中,狗的这些特征被称为特性Properties,而这些行为被称为行为methods。
在构建 Web 应用程序时,每个单元都将由一个对象表示。
假设您正在开发一款数字银行网络应用程序。该应用程序的一个实际实体示例是“人”。
该Person表示由和object组成的。propertiesmethods
这些属性的示例包括:名字、姓氏、出生日期、年龄、账户详情和分行。
可以Person实现的功能包括:登录、更改账户详情、查询账户余额、取款和存款等。
以下是该对象的示意图Person:
const Person = {
//properties
firstName: "Emmanuel",
lastName: "Kumah",
dob: "22/12/99",
branch: "Accra"
//methods
logIn(){
console.log(`Welcome ${this.firstName}`)
}
changeAccDetails(){
console.log('Account details changed')
}
checkAccBalance(){
console.log('Checking balance...')
}
withdrawFunds(){
console.log("Cash withdrawn")
}
depositFunds(){
console.log("Cash deposited")
}
}
无论何时Objects使用这些方法,应用程序的其他部分都不需要关心对象内部发生了什么。因为它为其他想要使用它的代码提供了一个接口,但它维护着自己的私有内部状态。
假设我们的数字银行应用程序有超过 25000 名用户,我们可能需要声明 25000 个对象,每个对象都有其唯一的properties标识符methods。
为了解决创建 25K 个唯一对象的繁琐任务,JS 引入了一个称为“对象”的概念。Classes
类和实例
类提供了一种将相似对象归为一类的方法。
例如,该应用程序上的超过 25000 名用户可以被归类为不同的用户群体。
因为每个用户都代表一个对象,并且共享一些共同的属性和方法,所以我们通常会创建一个概念定义来表示应用程序中可以存在的对象类型。
当我们在应用程序中处理大量对象时,我们可以通过定义一个模板或蓝图(即一个高级计划)来创建这些对象,从而避免为每个对象重复编写属性和方法,并加快开发速度。
此模板被称为Class
什么是类?
类指定了一种布局,用于创建具有预定义属性和方法的对象。
这好比一位建筑师设计了房屋建造蓝图。根据这张蓝图,我们可以建造尽可能多的建筑物,每栋建筑物都具备蓝图中定义的共同特征。
该类列出了创建新对象时将包含的数据和方法。
基本Class语法如下:
class MyClass {
constructor() { ... }
// class methods
method1() { ... }
method2() { ... }
method3() { ... }
...
}
现在,我们不再需要定义每个Person对象(从而产生超过 25K 个对象),而是使用class布局来帮助我们轻松创建这些对象。
我们将给类命名,User因为每个类Person都是应用程序的一个用户,并且我们将定义每个用户共有的所有属性properties和共同属性。methods
以下是该类的User定义。
class User{
// initialize properties
firstName,
lastName,
dob,
branch,
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//action performed by all users
logIn(){
console.log(`Welcome ${this.firstName}`)
}
}
以上定义了一个User类。
-
四
properites:名字、姓氏、出生日期、分支 -
该
constructor函数:用于创建对象 -
login()所有用户均可使用的方法。
实例
AClass本身不做任何事情,它只是作为创建特定对象的指南。我们创建的每个对象都会成为该类的一个实例。
实例是包含由类定义的数据和行为的对象。
构造函数
创建实例时,我们在类的主体中使用一个称为构造函数的特殊方法。
通常,构造函数是类定义的一部分。它允许我们为新实例中想要指定的属性赋值。
以下是该类中构造函数的代码片段User:
class User {
...
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
...
}
上面的代码中的构造函数接受四个参数,因此我们可以在创建新对象时为firstName、lastName和属性赋值。dobbranchUser
下一节,我们将学习如何从类创建对象。
从类创建新对象
我们使用该new运算符创建一个新对象(类的实例),该对象具有所有列出的方法和属性。
语法如下:
new constructor(arg1, arg2,...)
让我们User使用运算符创建前面示例中类的一个实例new。
//creating a new object from User class
const cus1 = new User (arg1, arg2,...)
何时new User(arg1, ag2, ...)调用:
-
已创建一个新对象。
-
该
constructor函数(与类同名)使用给定的参数运行,以便将这些值赋给新对象。
下面介绍一下我们如何在应用程序中从该类创建三个不同的对象。User
class User{
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the user
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
//create a user object from User class
const firstUser = new User('Emmanuel','Kumah','22/12/89','Accra')
const secondUser = new User('Robert','Taylor','12/12/77', "Accra")
const thirdUser = new User('Edmond',"Sims",'8/03/99', 'Accra')
//log the details
console.log(firstUser);
console.log(secondUser);
console.log(thirdUser)
-
上述操作会在我们的应用程序中创建三个 User 对象。
-
这三个对象均源自已定义的
User蓝图。由于我们使用了User蓝图,因此无需为每个创建的用户编写完整的代码(包括属性和方法)。
上述代码的输出结果如下:
-
每个对象都可以访问
logIn()类中声明的方法。 -
请查看以下代码。
//access the methods defined in the User class
firstUser.logIn()
"Welcome Emmanuel"
下一节,我们将探讨继承,它是面向对象编程的核心支柱之一。
遗产
我们的数字银行应用程序肯定需要能够登录并进行交易(存款和取款)的客户。
我们还需要经理来负责监督银行的各项活动。
Client和BranchManager将具有以下共同的属性和方法:
-
属性:名字、姓氏、出生日期和分行
-
方法:logIn()
除了登录应用程序外,分行经理还可以查看所有用户的详细信息及其交易记录。客户也可以存取现金。
让我们用以下方式表示“BranchManager和” Client:
class BranchManager {
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the branch manager
logIn(){
console.log(`Welcome ${this.firstName}`)
};
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
}
// Client class
class Client {
// initialize properties
firstName
lastName
dob
branch
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//actions performed by the branch manager
logIn(){
console.log(`Welcome ${this.firstName}`)
};
depositCash(){
console.log('Cash deposited')
}
withdrawCash(){
console.log('Cash withdrawn')
}
}
我们注意到,我们之前定义的User类与上面的Branch Manager 和 Client类有一些共同的属性和方法。
-
他们俩都有名字、姓氏、分行和出生日期。
-
他们俩都有这种
logIn()方法
由于它们共享某些共同的属性和方法,通常建议我们定义一个类User,该类包含应用程序中所有其他实体所需的所有公共属性和方法。这有助于减少冗余。
该类User定义如下:
class User {
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
//common method to all entities
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
-
上面定义的类
User被称为超类或父类 -
当我们声明一个父类时,我们可以声明子类,这些子类将继承父类的所有属性和方法。这种现象称为继承。
继承是指一个类能够从另一个类获得属性和特征,同时又拥有自身属性的能力。
在现实生活中,遗传是指孩子能够从父母那里获得某些特征,如身高、头发颜色、鼻子形状、智力等等。
同样地,在编程中,子类可以继承父类的所有属性和方法。
继承的目的是重用公共逻辑。
使用 extend 和 super 关键字继承
该关键字extend用于表示类之间的继承关系,并声明我们要继承的父类。
语法如下:
childClassName extends parentClassName{
//child class definition
}
使用关键字extends,我们可以指示BranchManager子Client类继承父类的所有属性和方法User。子BranchManager类Client还可以向自身添加额外的属性和方法。
下面的代码中,我们扩展了该类User,使其包含BranchManager和Client类。
class User {
// constructor function
constructor(firstName, lastName, dob, branch){
this.firstName = firstName;
this.lastName = lastName;
this.dob = dob;
this.branch = branch;
}
logIn(){
console.log(`Welcome ${this.firstName}`)
};
}
//Client class inherits from User
class Client extends User {
constructor(firstName, lastName, dob, branch, accType,amtDeposited){
//call the super before adding option
super(firstName, lastName, dob, branch);
//added new properties
this.accType = accType;
this.amtDeposited = amtDeposited
}
//additional methods
depositCash(){
console.log(`${this.firstName} deposited ${this.amtDeposited} USD`)
}
withdrawCash(){
console.log(`${this.firstName} withdrew cash`)
}
}
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
this.expLevl = expLevel;//added new props
}
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
}
//create two instances of the Client
const client1 = new Client('Ruby', 'Smit', '05/09/88', 'Accra', 'Savings', 20)
console.log(client1)
//access all methods of the Client class
client1.logIn()
client1.depositCash()
client1.withdrawCash()
// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)
console.log(firstManager)
//access the login method
firstManager.logIn()
//access the additional method
firstManager.viewUserDetails()
firstManager.viewTransactions()
-
父类是,
User而BranchManager和Client是子类。 -
使用关键字
extend,该类BranchManager将Client继承所有属性和方法User。 -
用于
super调用其父类的构造函数,以访问父类的属性和方法。 -
,
this.expLevl = expLevel并且允许我们分别向和类添加额外的this.accType = accType属性。this.amtDeposited = amtDepositedBranchManagerClient -
我们还添加了两个额外的方法
viewTransactions。viewUserDetailsBranchManager -
在
Cleint课程中,我们添加了depositCashandwithdrawCash方法。 -
因为 `
BranchManagerand`Client类继承自 `Userclass`,所以 `the`logIn()已自动添加,并且可以被 `class` 的实例访问。
继承的概念现在允许类的任何实例继承类中定义的所有属性和BranchManager方法。ClientUser
即使logIn()没有在类中定义BranchManager,Client我们仍然能够访问它。
上述代码的输出结果如下:
继承的重要性:
-
继承使我们能够定义一个类,该类继承父类的所有功能,并允许您添加更多功能。
-
它有助于代码重用,提供更简洁、更易于维护的代码,并消除冗余。
多态性
多态性允许同名方法在不同的类中有不同的实现。
在多态性中,子类可以重写从父类继承的方法。其目的是为了提高灵活性并增强代码的可重用性。
例如,可以在类中重写该logIn()方法。UserBranchManager
请看以下代码:
//BranchManager class inherits from User
class BranchManager extends User {
//same code snippet used earlier
...
//overwrite method in superclass
logIn(){
console.log(`Howdy ${this.firstName}`)
}
}
const manager = new BranchManager('Simon','Tagoe','13/02/2000',"Accra",14)
//access the logIn() method
manager.logIn()
// output will be
Howdy Simon
封装
封装是将数据及其操作方法封装在一个组件(类)中的过程。封装的核心思想是只允许对象本身访问状态数据。它用于隐藏类内部对象的值或状态,以防止未经授权的代码对其进行修改。
它的意义在于确保数据仅在对象内部可见,避免对象外部的任何直接访问。
封装鼓励信息隐藏,并通过消除实现细节来降低复杂性。
想象一下你使用电视遥控器,遥控器上有一些操作界面,例如电源按钮和数字按钮。你可以使用这些界面执行一些操作,例如打开或关闭电视、切换当前频道、保存收藏频道等等。
你只需使用这些接口,无需关心其工作原理。换句话说,接口的实际实现对你来说是隐藏的。
同样,在面向对象编程(OOP)中,你可以通过调用对象的方法来使用它。无论这些对象是你自己编写的,还是使用了第三方库,你的代码都不需要关心这些方法的内部工作原理。
对象中包含的数据只能通过公共接口(即对象自身的方法)访问。每当需要使用对象中包含的数据时,都需要在该对象中定义一个方法来处理该操作。
将数据从对象内部检索出来,然后编写单独的代码来对对象外部的数据执行操作,这种做法被认为是不好的做法。
例如,如果您想将一位拥有 5 年以上经验的分行经理晋升到下一级别,我们可能需要按如下方式实施:
//create a instance of Branch Manager class
const branchManager = new BranchManager('firstName','lastName', '04/1/80', 7)
// code snippet to promote a manager
if(branchManager.expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
在我们的系统中,可以在任何需要的地方使用晋升经理的代码。
如果采用这种实现方式,如果我们决定更改经理晋升的标准,我们将不得不找到应用程序中所有实现该功能的代码并进行更新。
然而,封装的思想要求对象内部的数据只能通过对象自身的方法访问。
最好promoteManager()在类中设置一个方法BranchManager,将所有逻辑集中在一个地方处理。这样,当逻辑需要更新时,就只需要在一个地方进行更新。
以下是推荐的步骤:
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
this.expLevl = expLevel;//added new props
}
//additional actions
viewTransactions(){
console.log(`${this.firstName} can view transactions `)
}
viewUserDetails(){
console.log(`${this.firstName} can view user details`)
}
//method to promote manager
promoteManager(){
if(this.expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
}
现在我们可以promoteManager()从类的实例中访问它。
//access method from the instance of the class
branchManager.promoteManager()
封装:私有属性和方法
可以将对象的内部数据设为私有,使其只能通过对象自身的方法访问,而不能从其他对象访问。
例如,如果我们不将expLevel属性设为私有,其他人就可以访问它并更改其值。this.expLevel = 10
要将属性设为私有,我们在属性名称后面加上下划线 ( _)。例如 ( this._propertyName)。
让我们把班级expLevl里的私密性设为私密BranchManager
class User {
...
}
//BranchManager class inherits from User
class BranchManager extends User {
constructor(firstName, lastName, dob, branch, expLevel){
//call the super before adding option
super(firstName, lastName, dob, branch);
//protected property
this._expLevl = expLevel;
}
...
//method to promote manager
promoteManager(){
if(this._expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
// create an instance of Branch manager
const firstManager = new BranchManager('Jose','Adams','22/09/1999',"Kumasi",12)
// try accessing the expLevl property and the output wil be undefined
console.log(firstManager.expLevl)
这种实现方式并不能真正使该属性私有,因此它被称为受保护属性。
为了使属性真正私有,我们需要在属性名称前加上 `a`,#并且该属性应该定义在任何方法之外。例如:#firstName
我们还可以保护一个方法,防止它在类外部被访问。
以下是保护该promoteManager方法的方法:
//protect the promoteManager method
_promoteManager(){
if(this._expLevl >= 5){
console.log('You will be promoted to the next rank')
}else {
console.log('better luck next time')
}
}
//try accessing the method
manager.promoteManager()
代码的输出结果为:
Uncaught TypeError: manager.promoteManager is not a function
封装技术的优点概述如下:
-
对象内的数据不能被应用程序完全不同部分的外部代码意外修改。
-
当我们使用某个方法时,我们只需要知道结果,而不需要关心其内部实现。
-
功能是在逻辑位置定义的:也就是数据保存的位置,因此很容易更改应用程序的功能。
抽象
抽象是指隐藏或忽略不重要的细节,从而使我们能够对正在实现的事物有一个概览性的了解。
抽象的目的是通过向用户隐藏所有无关细节来处理复杂性,从而实现更易于理解和维护的简化设计。
通常的做法是通过指定与对象交互的方法,并通过接口来实现。这些方法的具体实现细节都隐藏在这些接口之后。
这使得程序员能够以更高的抽象层次处理对象,而无需担心其底层实现细节。
例如,当你购买智能手机时,你可能只对以下几点感兴趣:
-
如何打开或关闭手机
-
如何拨打电话
-
如何通过互联网获取信息
诸如此类的底层细节
-
手机如何连接到互联网以获取指定信息
-
手机开机或关机时,内部发生了什么?
-
陀螺仪传感器的工作原理:使你能够旋转手机
-
振动、环境温度、磁场传感器等的工作原理与您无关,因此这些细节可以隐藏,从而使手机更易于使用。
抽象旨在简化。例如,ArrayJavaScript 中的对象允许使用单个变量名存储多个元素的集合。
它包含以下方法:
-
Array.length这样就可以获取数组中元素的数量。 -
Array.push()将元素推入数组等
使用这些方法时,您无需了解定义Array对象的全部细节。这些底层细节已被抽象化(移除),以便于您轻松使用这些方法。我们的目标是让您只需使用公开的方法即可完成特定任务。
抽象的一个主要好处是它能降低变更的影响。Array例如,对象的开发者可以更改其内部实现来提升性能。但是,只要对象中仍然存在array.push()相应array.length的方法,你就可以像以前一样继续使用这些Array方法,而不会受到变更的负面影响。
概括
-
面向对象编程的核心思想是将程序分成更小的部分,并让每个部分负责管理自己的状态。
-
类指定了一种布局,用于创建具有预定义属性和方法的对象。
-
继承是指一个类能够从另一个类获得属性和特征,同时又拥有自身属性的能力。
-
多态性允许同名方法在不同的类中有不同的实现。
-
封装是将数据和处理数据的方法封装在一个组件(类)中的过程。
-
抽象是指隐藏或忽略不重要的细节,从而使我们能够对正在实现的事物有一个概览性的了解。
如果您觉得这篇文章对您有所帮助,或者发现了可以改进的地方,请留言评论。也请您将这篇文章分享到您的社交网络,它或许能对其他人有所帮助。
文章来源:https://dev.to/efkumah/the-easy-approach-to-learning-object-orient-programming-in-javascript-5e24

