面向对象编程

时间:2023-03-06 08:08:56

在编写软件时,你所做的大部分工作就是创建和连接多个值和方法,让他们一起工作,以便提供应用程序的功能。面向对象编程可以帮助你更容易地,并且是声明式地实现这些功能。

在这篇文章中,你将了解到在JavaScript中开始使用类和面向对象编程方法所需要的一切。

前置知识

在阅读本篇文章之前,你需要掌握JavaScript的基础知识。

面向对象编程

面向对象编程(OOP)是一种编程范式,在大型、复杂和积极维护的项目中,OOP每天都在被使用。

OOP使你更容易创建和管理你应用程序的许多部分,并在不使它们相互依赖的情况下将它们连接起来。接下来,让我们看看OOP的四个主要概念。

抽象

OOP中的抽象是指只向用户公开必要的功能,同时隐藏复杂的内部工作,使程序更容易使用和理解。

举例来说,当你在手机上发送消息时,所有将你的信息传递给对方的函数和逻辑都是隐藏的,因为你不需要知道它们是如何工作的。

同样地,在编程中,如果你正在构建一个帮助金融app验证用户身份和银行信息的API,使用你API的开发者不需要知道你使用的是哪种数据库系统,也不需要知道你如何调用你的数据库。他们唯一需要知道的是要调用的函数,以及他们需要提供的参数。

抽象有助于降低复杂性,增加可用性,并使应用程序的变化不那么具有破坏性。

封装

封装是将相关代码捆绑在一个独立单元中的过程。封装使代码的其他部分无法改变应用程序捆绑部分的工作方式,除非你显式地进入该单元并改变它们。

举例来说,如果你正在建立一个航班预订的API,把搜索航班的代码和预订航班的代码分开是有意义的。这样一来,两个不同的开发者就可以无缝地在每个部分工作而不发生冲突,因为每个开发者都没有理由直接操作对方的代码。

封装有助你降低复杂性,增加代码可用性。

继承

OOP中的继承降低了代码重复性,使你能够通过继承应用程序部分的属性和方法,在其他地方构建你的应用程序。

OOP中继承的一个重要优势是降低了代码重复性。

多态

在编程中,多态是一个术语,用来描述一个代码或程序,它可以通过根据给定的数据返回一个响应或结果来处理多个类型的数据。

举例来说,你有一个用来向产品目录添加产品的表单,并有三种不同类型的产品。通过多态,你可以创建一个单一类方法来格式化所有种类的产品,然后再将它们添加到数据库中。

多态可以帮助你消除复杂性和不必要的ifswitch语句,因为在编写复杂的程序时,这些语句会变得冗长。

接下来我们来看看JavaScript对象。

JavaScript对象

JavaScript中的对象是一个无序的键值对集合,也就是属性和值。对象的键可以是字符串,值可以是任何类型。

接下来,我们来看看如何在JavaScript中创建对象。

创建对象

在JavaScript中创建一个对象相当容易:

const car = {
    name: 'Ford',
    year: 2015,
    color: 'red',
    description: function () {
    return `${this.name} - ${this.year} - ${this.color}`;
    }
}

上述代码声明了一个car对象,对象属性包括nameyearcolor以及description函数。

访问对象属性

在JavaScript中有两种方式访问对象属性。我们接着往下看:

使用点符号

下面的例子展示了如何使用点符合来访问对象属性。

const country = {
    name: 'Spain',
    population: 4000000,
    description: function () {
    return `${this.name} - ${this.population}`;
    }
}

如果你有一个像上面所示的对象,你可以使用objectName.keyName的格式,它应该返回给定键的值:

console.log(country.name); // returns 'Spain'

使用数组符号

下面的例子展示了如何使用数组符号来访问对象属性。

const job = {
  role: "Software Engineer",
  'salary': 200000,
  applicationLink: "meta.com/careers/SWE-role/apply",
  isRemote: true,
};

如果你有一个像上面所示的对象,你可以使用objectName[keyName]的格式,它应该返回给定键的值:

console.log(job[role]); // returns 'Software Engineer'

此外,你只能用数组符号来访问salary属性。试图用点符号来获取它将返回一个错误:

console.log(job.'salary'); // SyntaxError: Unexpected string

接下来,我们来看看如何修改对象属性。

修改对象属性

你可以在JavaScript动态添加、编辑和删除对象属性。

编辑属性

你可以使用赋值=操作符来修改对象的值。这里一个例子:

const person = {
  name: "John",
  age: 30,
  job: "Software Developer",
  country: "Nigeria",
  car: "Ford",
  description: function () {
    return `${this.name} - ${this.age} - ${this.job.role} - ${this.country.name} - ${this.car.name}`;
  },
};

你还可以更改上述对象中name的值:

person.name = "Smith Doe";
console.log(person.name); // returns "Smith Doe"

添加新属性

其他语言中的对象与JavaScript中的对象之间的一个显著区别是,在创建后可以向对象添加新的属性。

为了向对象中添加新属性,你可以使用点符号:

// adding a new `race` property
person.race = "Asian";
console.log(person.race); // returns "Asian"

删除对象属性

JavaScript允许你通过使用delete关键字从一个对象中删除属性:

delete person.race;
console.log(person.race); // returns 'undefined'

注意:你只能删除现有的对象属性。

检查属性

在向一个对象添加或删除属性之前,确定该对象上是否存在该属性是一个很好的主意。这个看似简单的检查将为你节省几个小时的时间来调试一个由重复值引起的错误。

要确定一个属性是否存在于一个对象上,你可以使用in关键字:

console.log('name' in person) // returns true
console.log('race' in person) // returns false

上面的代码对于name的检查返回true,因为name存在,而对于被删除的race属性返回false

现在你知道了什么是对象以及如何使用它们,让我们通过学习类来迈出JavaScript的OOP的下一步。

在编程中,类是由程序员定义的一种结构,然后用来创建同一类型的多个对象。例如,如果你正在建立一个处理各种汽车的应用程序,你可以创建一个Car类,它具有适用于所有车辆的基本功能和属性。然后,你可以用这个类来制作其他的汽车,并为它们添加每种汽车所特有的属性和方法。

为了扩展你在前面的例子中看到的对象,如果你想创建另一个job对象,你需要创建这样的东西:

const job2 = {
  role: "Head of Design",
  salary: 175000,
  applicationLink: "amazon.com/careers/hod-role",
  isRemote: false,
};

然而,正如你所看到的,上面创建多个对象的方式容易出错,并且不可扩展。因为你不可能在每次需要创建一个job时都写出这些,从而产生100个job对象。这时类就派上用场了。

创建类

你可以创建一个Job类来简化创建多个job

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
}

上述代码创建了一个Job类,具有rolesalaryapplicationLink以及isRemote属性。现在你可以使用new关键字创建不同的job

let job1 = new Job(
  "Software Engineer",
  200000,
  "meta.com/careers/SWE-role/apply",
  true
);

let job2 = new Job(
  "Head of Design",
  175000,
  "amazon.com/careers/hod-role",
  false
);

上面的代码创建了两个不同的job,包含所有的必填字段。让我们通过在控制台中打印出两个job来看看这是否奏效:

console.log(job1);
console.log(job2);

打印结果为:

面向对象编程

图中显示了两个job和它们在控制台中的所有属性。

this关键字

this关键字被认为是最令人迷惑的关键字,因为它包含有不同的含义,这取决于在代码中什么位置出现。

在上面的例子中,this关键词指的是用Job类创建的任何对象。因此,constructor方法内部的this.role = role; 意味着,将你正在创建的这个对象的role属性定义为给该构造函数的任何值。

另外请注意,在创建一个新的Job对象时,你给出的初始值必须是按顺序的。例如,你像这样创建一个job3对象:

let job3 = new Job(
  "netflix.com/careers/HOE-role",
  true,
  "Head of Engineering"
);

console.log(job3)

上述代码创建了一个新的job3对象,该对象有着错误的属性顺序,并缺失了isRemote属性。你会在控制台中得到以下结果:

面向对象编程

上面的图片显示了job3对象在控制台中打印出来的样子。请注意,isRemoteundefined

接下来,我们来看看如何给类添加方法。

类方法

当创建类时,你可以添加多个属性。为了在类内部添加方法,你可以在constructor函数后面添加:

class Vehicle {
  constructor(type, color, brand, year) {
    this.type = type;
    this.color = color;
    this.brand = brand;
    this.year = year;
  }
  start() {
    return "Vroom! Vroom!! Vehicle started";
  }
  stop() {
    return "Vehicle stopped";
  }
  pickUpPassengers() {
    return "Passengers picked up";
  }
  dropOffPassengers() {
    return "Passengers dropped off";
  }
}

上述代码定义了一个具有typecolorbrandyear属性的Vehicle类,同时具有startstoppickUpPassengersdropOffPassengers方法。

为了运行对象中的方法,可以使用点符号:

const vehicle1 = new Vehicle("car", "red", "Ford", 2015);
const vehicle2 = new Vehicle("motorbike", "blue", "Honda", 2018);

console.log(vehicle1.start()); // returns 'Vroom! Vroom!! Vehicle started'
console.log(vehicle2.pickUpPassengers()); // returns "Passengers picked up"

接下来,让我们看看计算属性。

计算属性

编程在很大程度上取决于值的变化,类似于你不想硬编码类属性的大部分值,你可能会有一些基于某些值而变化的动态属性名称。你可以使用计算属性来做到这一点;让我们看看是如何做到的。

例如,在创建工作列表API时,你可能希望开发者能够将applyThrough函数名称改为另一个词,如applyThroughLinkedInapplyThroughIndeed,这取决于他们的平台。要使用计算属性,你需要用方括号把属性名称包起来:

let applyThrough = "applyThroughIndeed";

class Job {
  constructor(role, salary, applicationLink, isRemote) {
    this.role = role;
    this.salary = salary;
    this.applicationLink = applicationLink;
    this.isRemote = isRemote;
  }
  [applyThrough]() {
    if (applyThrough === "applyThroughLinkedin") {
      return `Apply through Linkedin`;
    } else if (applyThrough === "applyThroughIndeed") {
      return `Apply through Indeed`;
    }
  }
}

上面的代码声明了applyThrough变量的字符串值为 "applyThroughIndeed",以及一个可以调用job1.applyThroughIndeed()的计算方法[applyThrough]

计算属性使其他开发者更容易定制他们的代码。

Getters and Setters

在团队中编写代码时,你想限制谁可以改变代码库的某些部分,以避免出现bug。建议在OOP中,某些变量和属性在必要时应该被隐藏。

接下来,让我们学习getset关键字是如何工作的。

Getters

当构建热衷于确保用户隐私的应用程序时,例如,法律问题管理应用程序,你要控制谁可以访问用户的数据,如姓名、电子邮件和地址。get关键字可以帮助你实现这一目标。你可以限制谁可以访问信息:

class Client{
  constructor(name, age) {
    this._name = name;
    this._age = age;
  }
  get name() {
    if (userType === "Lawyer") {
      return this._name;
    } else {
      return "You are not authorized to view this information";
    }
  }
}

上面的代码声明了一个Client类,其属性是nameage,还有一个getter,只在用户是律师时返回姓名。如果你试图以助理的身份访问名字,你会得到一个错误:

let userType = "Assistant";
const person = new Client("Benjamin Adeleye", 24);
console.log(person.name); // returns 'You are not authorized to view this information'

注意:将this.name改为this._name以避免命名冲突。

Setters

set关键字是get关键字的反面。get关键字用于控制谁可以访问属性,而set关键字控制谁可以改变属性的值。

为了了解set关键字如何工作,让我们通过添加一个setter来扩展前面的例子:

set name(newName) {
  if (userType === "Lawyer" && verifiedData === true) {
    this._name = newName;
  } else {
    console.log("You are not authorized to change this information");
  }
}

上面的代码声明了一个set方法,只有在用户是律师并且文件已经被验证的情况下,才允许对名字进行更改:

let userType = "Lawyer";
let verifiedData = false;
let client = new Client("Benjamin Adeleye", 30);
client.name = "Adeleye Benjamin";
console.log(client.name); // returns 'You are not authorized to change this information'

注意:以getset方法为前缀的方法分别称为gettersetter函数。

接下来,让我们看看静态属性和方法。

静态值

有时你想创建绑定到类而不是类的实例的属性和方法。例如,你可能想要一个计算数据库中客户数量的属性,但你不希望这个值绑定到类的实例上。

静态属性

为了追踪数据库客户数量,你可以使用static关键字:

static clientCount = 0;

上述代码声明了一个静态clientCount属性,其值为0。你可以像这样来访问静态属性:

let cl = new Client("Benjamin Adeleye", 30);
console.log(Client.clientCount); // returns 0

注意:试图使用console.log(cl.clientCount);访问静态属性会返回undefined,因为静态属性被绑定到类而不是实例上。

接下来,让我们看看静态方法。

静态方法

创建静态方法跟创建静态属性十分类似,因为你只需要在方法名称前加上static关键字:

static increaseClientCount() {
  this.clientCount++;
}

上面的代码声明了一个静态的increateClientCount方法,该方法每次被调用时都会增加clientCount

静态方法和属性使得创建可以直接用于类而不是实例的辅助函数变得容易。

私有值

ECMAScript2022的更新支持JavaScript类中的私有值。

私有字段和方法将类的封装提升到了一个新的水平,因为你现在可以创建只能在类声明的大括号内使用的属性和方法,而在这些大括号外的任何代码都无法访问它们。

接下来让我们看看私有属性。

私有属性

你可以通过在变量前加上#号来声明类中的私有属性。让我们通过为每个客户添加一个唯一的ID来改进Client类的部分:

class Client {
  #client_unique_id = "";
  constructor(name, age, id) {
    this._name = name;
    this._age = age;
    this.#client_unique_id = id;
  // same as Client class...
  }

上面的代码声明了一个私有的#client_unique_id变量,只能在类声明中使用和访问。试图在类外访问它将返回一个错误:

let cl = new Client("Benjamin Adeleye", 30, "##34505833404494");
console.log(cl.name);
console.log(cl.#client_unique_id); // returns Uncaught SyntaxError: Private field '#client_unique_id' must be declared in an enclosing class

私有方法

如前所述,私有方法只能在类声明中访问。为了学习私有方法的工作原理,我们将添加一个私有方法,从数据库中获取客户的案件文件文档:

#fetchClientDocs(id) {
   return "Fetching client with id: " + id;
}

上述代码现在可以在一个公共函数中使用,用户将调用该函数来获取客户端的文件。私有函数的本质是将所有的底层认证和对数据库的调用从使用该代码的用户或开发人员那里隐藏起来。

注意:你也可以创建私有静态、gettersetter函数。

接下来,我们来了解如何链式类方法。

方法链

作为一个开发者,你可能最喜欢做的事情之一就是用尽可能少的代码实现一个功能。你可以在JavaScript中通过链式方法来实现这一点。例如,当一个用户登录到你的应用程序时,你想把用户的状态改为"在线",当他们退出时,你再把它改回"离线":

class Twita {
  constructor(username, offlineStatus) {
    this.username = username;
    this.offlineStatus = offlineStatus;
  }
  login() {
    console.log(`${this.username} is logged in`);
    return this;
  }
  setOnlineStatus() {
    // set the online status to true
    this.offlineStatus = false;
    console.log(`${this.username} is online`);
    return this;
  }
  setOfflineStatus() {
    // set the offline status to true
    this.offlineStatus = true;
    console.log(`${this.username} is offline`);
    return this;
  }
  logout() {
    console.log(`${this.username} is logged out`);
    return this;
  }
}

上述代码声明了一个Twita类,具有usernameofflineStatus属性,并具有loginlogoutsetOnlineStatussetOfflineStatus方法。为了链式使用这些方法,你可以使用点符号:

const user = new Twita("Adeleye", true);
user.login().setOnlineStatus().logout().setOfflineStatus();

上述代码将在user对象上依次运行所有的函数并返回一个响应:

面向对象编程

注意:你需要在每个函数的末尾通过返回this来返回当前对象。

接下来,我们来看看如何通过继承来建立在现有的类上。

类继承

在处理对象时,你很可能会遇到这样的情况:你需要创建一个与你代码中已经存在的对象非常相似的对象,但你知道它们不可能是一样的。例如,当建立一个电子商务应用程序时,你会有一个Product类,它有namepricedescription以及image等属性,以及formatPriceaddToCart等方法。

然而,如果你有多种不同规格的产品,怎么办?

  • 带有作者、重量和页面详情的书籍。
  • 家具的长度、宽度、高度和木材类型的详细信息。
  • 电影光盘的尺寸、时间和内容详情。

在这种情况下,为每个产品创建一个类将导致大量的代码重复,这是OOP和一般编程的主要规则之一:don't repeat yourself(DRY)。

类继承允许你在其他对象的基础上创建对象。例如,你可以通过创建一个Product类来解决上面提到的问题:

class Product {
  constructor(name, price, description, image) {
    this.name = name;
    this.price = price;
    this.description = description;
    this.image = image;
  }
  formatprice() {
    return `${this.price}$`;
  }
  addToCart() {
    cart.add(this);
  }
}

接着,使用extends关键字为每个产品类型创建一个子类:

class Book extends Product {
  constructor(name, price, description, image, weight, pages, author) {
    super(name, price, description, image);
    this.author = author;
    this.weight = weight;
    this.pages = pages;
  }
}

class Movie extends Product {
  constructor(
    name,
    price,
    description,
    image,
    size,
    contentDescription,
    duration
  ) {
    super(name, price, description, image);
    this.size = size;
    this.contentDescription = contentDescription;
    this.duration = duration;
  }
}

class Furniture extends Product {
  constructor(
    name,
    price,
    description,
    image,
    woodType,
    length,
    height,
    width
  ) {
    super(name, price, description, image);
    this.woodType = woodType;
    this.length = length;
    this.height = height;
    this.width = width;
  }
}

上述代码通过扩展Product类来声明BookMovieFurniture产品类型。

在上面的代码中,有两个新的关键字:extendssuper。接下来,让我们来看一下它们。

extends关键字

extends关键字是不言自明的;它被用来扩展另一个类的能力。在我们的例子中,我们用它通过扩展Product类来创建BookMovieFurniture类。

super关键字

super关键字消除了你需要为每个新的子类重复进行的多次声明。例如,上述代码中调用的super函数取代了以下代码:

this.name = name;
this.price = price;
this.description = description;
this.image = image;

还记得DRY吗?这样做的原因是为了不重复上述代码,因为它已经写在了Product类里面。

如果子类不需要构造函数,super函数可以被忽略:

class Animal {
  constructor(name, species, color) {
    this.name = name;
    this.species = species;
    this.color = color;
  }
  makeSound() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Bird extends Animal {
  fly() {
    console.log(`${this.name} flies.`);
  }
}

上述代码声明了一个父类Animal和一个子类Bird,该类不需要构造函数来运行,因为它没有在构造函数中声明任何新变量。因此,下面的代码应该可以运行:

const bird = new Bird('Chloe', 'Parrot', 'Green'); 
console.log(`${bird.name} is a ${bird.color} ${bird.species}`);

上述代码可以运行,即使Bird类里面没有namecolor或者species

面向对象编程

如果你只需要给子类添加方法的话,你不需要调用super或者重复相同的构造函数。

总结

在本教程中,你了解了JavaScript中的面向对象编程和类,以及它如何帮助你保持代码的清洁、干燥和可重用。我们涵盖了面向对象编程的四个核心概念,包括抽象、封装、继承和多态。

你还了解到,在你的代码中使用面向对象的编程范式和类有很多优点,从改善应用结构和代码的可重用性到降低代码的复杂性。

以上就是本文的全部内容,如果对你有所帮助,欢迎收藏、点赞、转发~