发布于 2026-01-06 0 阅读
0

JavaScript 和面向对象编程

JavaScript 和面向对象编程

本文面向没有任何面向对象编程 (OOP) 基础的 JavaScript 学生。我主要关注 OOP 中仅适用于 JavaScript 而非通用 OOP 的部分。因此,我略过了多态,因为我认为它更适合静态类型语言。

你为什么需要知道这些?

你选择 JavaScript 作为你的第一门编程语言吗?你想成为一名顶尖的开发人员,参与开发代码量超过十万行的巨型企业系统吗?

除非你学会完全接受面向对象编程,否则你将会彻底迷失方向。

不同的思维模式

在足球比赛中,你可以采取稳固的防守策略,也可以从边路长传进攻,或者发起猛攻。所有这些策略的目标都只有一个:赢得比赛。

编程范式也是如此。解决问题和设计解决方案的方法有很多种。

面向对象编程(OOP)是现代应用程序开发的范式,并受到 Java、C# 或 JavaScript 等主流语言的支持。

面向对象范式

从面向对象编程(OOP)的角度来看,应用程序是由相互通信的“对象”组成的集合。这些对象基于现实世界的事物,例如库存中的产品或员工记录。对象包含数据,并根据这些数据执行一些逻辑。因此,OOP 代码非常容易理解。真正困难的是如何首先将应用程序拆分成这些小对象。

如果你像我第一次听到这个概念时一样,完全不明白它的意思——听起来非常抽象。这种感觉完全正常。更重要的是,你了解了这个概念,记住了它,并尝试在你的代码中应用面向对象编程(OOP)。随着时间的推移,你会积累经验,并让你的代码越来越符合这个理论概念。

教训:基于现实世界对象的面向对象编程可以让任何人阅读你的代码并理解代码的运行机制。

以物品为中心装饰

以物品为中心装饰
一个简单的例子可以帮助你理解 JavaScript 如何实现面向对象编程 (OOP) 的基本原则。考虑一个购物场景:你将商品放入购物车,然后计算总价。如果你运用现有的 JavaScript 知识,不使用 OOP 来编写这个场景的代码,代码会是这样的:

const bread = {name: 'Bread', price: 1};
const water = {name: 'Water', price: 0.25};

const basket = [];
basket.push(bread);
basket.push(bread);
basket.push(water);
basket.push(water);
basket.push(water);

const total = basket
  .map(product => product.price)
  .reduce((a, b) => a + b, 0);

console.log('one has to pay in total: ' + total);
Enter fullscreen mode Exit fullscreen mode

面向对象编程(OOP)的视角让编写更优质的代码变得更容易,因为它让我们像在现实世界中一样思考对象。由于我们的用例包含一个商品篮,我们已经有了两种对象——篮子对象和商品对象。

购物用例的面向对象编程版本可以这样写:

const bread = new Product("bread", 1);
const water = new Product("water", .25)

const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.printShoppingInfo();
Enter fullscreen mode Exit fullscreen mode

如您在第一行所见,我们使用关键字new加上类名(如下所述)来创建一个新对象。这将返回一个对象,我们将其存储到变量 bread 中。我们对变量 water 重复此操作,并采用类似的方法创建变量 basket。将这些商品添加到购物车后,最终会打印出您需要支付的总金额。

这两个代码片段之间的区别显而易见。面向对象编程版本几乎就像真正的英语句子一样,很容易理解其含义。

教训:以现实世界事物为模型的对象由数据和函数组成。

类作为模板

类作为模板
在面向对象编程(OOP)中,我们使用类作为创建对象的模板。对象是“类的实例”,而“实例化”则是基于类创建对象的过程。代码定义在类中,但只有在已创建的对象中才能执行。

你可以把类看作是汽车的蓝图。它们定义了汽车的属性,例如扭矩和马力,内部功能,例如空燃比,以及公开可访问的方法,例如点火。然而,只有当工厂实例化汽车后,你才能转动钥匙并驾驶它。

在我们的用例中,我们使用 Product 类来实例化两个对象:面包和水。当然,这些对象需要您在类中提供相应的代码。代码如下:

function Product(_name, _price) {
  const name = _name;
  const price = _price;

  this.getName = function() {
    return name;
  };

  this.getPrice = function() {
    return price;
  };
}

function Basket() {
  const products = [];

  this.addProduct = function(amount, product) {
    products.push(...Array(amount).fill(product));
  };

  this.calcTotal = function() {
    return products
      .map(product => product.getPrice())
      .reduce((a, b) => a + b, 0);
  };

  this.printShoppingInfo = function() {
    console.log('one has to pay in total: ' + this.calcTotal());
  };
}
Enter fullscreen mode Exit fullscreen mode

JavaScript 中的类看起来像函数,但使用方法不同。函数名就是类名,并且首字母要大写。由于类不返回任何值,我们不能像通常那样调用函数const basket = Product("bread", 1);,而是添加关键字 `new`,例如 `new` const basket = new Product("bread", 1);

函数内部的代码是构造函数,每次实例化对象时都会执行。Product 对象有参数 `a`_name和 `b` _price。每个新对象都会将这些值存储到自身中。

此外,我们还可以定义对象提供的函数。这些函数通过在名称前加上 `this` 关键字来定义,使其可以从外部访问(参见封装)。请注意,这些函数拥有对对象属性的完全访问权限。

Basket 类创建新对象时不需要任何参数。实例化一个新的 Basket 对象只会生成一个空的商品列表,程序随后可以填充该列表。

课程:类是运行时生成对象的模板。

封装

封装
你可能会遇到另一种声明类的方法:

function Product(name, price) {
  this.name = name;
  this.price = price;
}
Enter fullscreen mode Exit fullscreen mode

注意将属性赋值给变量this。乍一看,这似乎是一个更好的版本,因为它不再需要 getter 方法(getName 和 getPrice),因此代码更简洁。

很遗憾,您现在已授予外部对这些属性的完全访问权限。因此,任何人都可以访问和修改它们:

const bread = new Product('bread', 1)
bread.price = -10;
Enter fullscreen mode Exit fullscreen mode

这样做并不可取,因为它会增加应用程序的维护难度。例如,如果您添加验证代码来防止价格低于零,会发生什么情况?任何直接访问价格属性的代码都会绕过验证。这可能会引入难以追踪的错误。另一方面,使用对象 getter 方法的代码则保证会经过对象的价格验证。

对象应该对其数据拥有独占控制权。换句话说,对象“封装”了自己的数据,防止其他对象直接访问这些数据。访问数据的唯一方法是通过对象内部编写的函数间接访问。

数据和处理(即逻辑)密不可分。对于大型应用程序而言,这一点尤为重要,因为在这些应用程序中,将数据处理限制在特定区域内至关重要。

如果运用得当,面向对象编程(OOP)能够从设计上实现模块化,这正是软件开发中的圣杯。它避免了令人闻风丧胆的“意大利面条式代码”,在这种代码中,所有功能紧密耦合,你根本无法预知修改一小段代码会带来什么后果。

在我们的例子中,Product 类的对象在初始化后不允许您更改价格或名称。Product 实例是只读的。

教训:封装可以防止通过对象自身的函数以外的方式访问数据。

遗产

继承人
继承允许你通过扩展现有类并添加属性和函数来创建新类。新类“继承”了其父类的所有特性,避免了从头开始编写新代码。此外,对父类所做的任何更改都会自动反映到子类中,从而大大简化了更新过程。

假设我们有一个名为 Book 的新类,它包含名称、价格和作者三个属性。通过继承,我们可以说 Book 与 Product 类似,只是 Book 多了作者属性。我们称 Product 为 Book 的超类,而 Book 为 Product 的子类。

function Book(_name, _price, _author) {
  Product.call(this, _name, _price);
  const author = _author;

  this.getAuthor = function() {
    return author;
  };

}
Enter fullscreen mode Exit fullscreen mode

Product.call请注意第一个参数旁边的附加信息this。请注意:虽然 book 类提供了 getter 方法,但它仍然无法直接访问 name 和 price 属性。book 类必须从 Product 类中调用这些数据。
现在您可以将 book 对象添加到购物车,而不会遇到任何问题:

const faust = new Book('faust', 12.5, 'Goethe');
basket.addProduct(1, faust);
Enter fullscreen mode Exit fullscreen mode

Basket 需要一个 Product 类型的对象,而 book 通过 Book 继承自 Product,因此它也是一个 Product 对象。

教训:子类可以从父类继承属性和函数,同时添加自己的属性和函数。

JavaScript 和面向对象编程

你会发现创建 JavaScript 应用程序时使用了三种不同的编程范式。它们分别是原型编程、面向对象编程和函数式编程。

原因在于 JavaScript 的历史。最初,它是基于原型开发的。JavaScript 并非为大型应用程序而设计的语言。

与创始人的初衷相反,开发者们越来越多地将 JavaScript 用于更大型的应用程序。面向对象编程(OOP)正是在最初基于原型的技术基础上发展起来的。

下面展示的是基于原型的方法,它被视为构建类的“经典和默认方式”。遗憾的是,它不支持封装。

尽管 JavaScript 对面向对象编程 (OOP) 的支持不如 Java 等其他语言,但它仍在不断发展。ES6 版本新增了一个专用class关键字。它在内部的作用与原型属性相同,但可以减少代码量。然而,ES6 类仍然缺少私有属性,这也是我坚持使用“旧方法”的原因。

为了完整起见,这里我们分别用 ES6 类和原型(经典和默认)方式编写 Product、Basket 和 Book 类。请注意,这些版本不提供封装:

// ES6 version

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
}

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

class Basket {
  constructor() {
    this.products = [];
  }

  addProduct(amount, product) {
    this.products.push(...Array(amount).fill(product));
  }

  calcTotal() {
    return this.products
      .map(product => product.price)
      .reduce((a, b) => a + b, 0);
  }

  printShoppingInfo() {
    console.log('one has to pay in total: ' + this.calcTotal());
  }
}

const bread = new Product('bread', 1);
const water = new Product('water', 0.25);
const faust = new Book('faust', 12.5, 'Goethe');

const basket = new Basket();
basket.addProduct(2, bread);
basket.addProduct(3, water);
basket.addProduct(1, faust);
basket.printShoppingInfo();
Enter fullscreen mode Exit fullscreen mode
//Prototype version

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Book(name, price, author) {
  Product.call(this, name, price);
  this.author = author;
}
Book.prototype = Object.create(Product.prototype);
Book.prototype.constructor = Book;

function Basket() {
  this.products = [];
}
Basket.prototype.addProduct = function(amount, product) {
  this.products.push(...Array(amount).fill(product));
};
Basket.prototype.calcTotal = function() {
  return this.products
    .map(product => product.price)
    .reduce((a, b) => a + b, 0);
};
Basket.prototype.printShoppingInfo = function() {
  console.log('one has to pay in total: ' + this.calcTotal());
};
Enter fullscreen mode Exit fullscreen mode

教训:面向对象编程(OOP)是在 JavaScript 发展后期才加入的。

概括

作为一名学习 JavaScript 的新手程序员,需要时间才能充分理解面向对象编程。在这个早期阶段,需要理解的重要内容是面向对象编程范式所基于的原则及其带来的好处:

  • 基于现实世界事物建模的对象是任何基于面向对象编程的应用程序的核心。
  • 封装可以保护数据免受不受控制的访问。
  • 对象具有对对象所包含的数据进行操作的函数。
  • 类是用于实例化对象的模板。
  • 继承是避免冗余的有力工具。
  • 面向对象编程(OOP)虽然比其他编程范式更冗长,但更容易阅读。
  • 由于面向对象编程 (OOP) 在 JavaScript 的发展后期才出现,因此您可能会遇到使用原型或函数式编程技术的较早代码。

延伸阅读

文章来源:https://dev.to/rainerhahnekamp/javascript-and-object-driven-programming-55k6