Stop Using JavaScript Classes!

2025-06-09

停止使用 JavaScript 类!

《头脑特工队》中愤怒的皮克斯角色

你是否深陷于充斥着类的 JavaScript 代码库?你是否刚接触 JavaScript,并试图复用面向对象语言的模式?你是否想过,在 JS 中是否有更好的实现方式?

别担心!通过学习使用模块而不是类来编写符合 JavaScript 习惯的代码,你可以编写更简洁、更易读、更易于维护的代码,并提供更愉悦的工程体验。在本文中,我将向你展示为什么在符合 JavaScript 习惯的代码中应该避免使用类,以及为什么模块应该是你的首选。

*注意:在继续之前,您应该对面向对象编程(OOP)JavaScript (JS)有所熟悉。

问题:

下面是我经常在野外看到的 JavaScript 类型的示例:

class ContrivedClass {
  constructor(db) {
    this.db = db
  }

  getPerson() {
    return this.db.getPerson();
  }
}
Enter fullscreen mode Exit fullscreen mode
// somewhere else in the code, this class is only called once

const contrivedClass = new ContrivedClass(db);
const person = contrivedClass.getPerson();
// do something with person
Enter fullscreen mode Exit fullscreen mode

上面你看到的 JavaScript 类只有一个方法和一个用于注入依赖项的构造函数参数,而且该类只会实例化一次。而我的反应总是一样的……

茫然地盯着电脑

我为什么会有这种反应?因为在 JavaScript 中,有很多更好的代码编写方法,但在某些代码库中,类似乎比迈克尔·乔丹在《空中大灌篮》中的表演还要精彩!(对不起,勒布朗。)

你好像有点不开心。什么是课程?你为什么对课程有意见?

要理解为什么不应该将类作为组织 JavaScript 代码的首选方式,我们必须首先了解类是什么以及它们的优势所在。根据 Oracle 的Java 文档,在面向对象编程中,对象“将其状态存储在字段(某些编程语言中称为变量)中,并通过方法(某些编程语言中称为函数)公开其行为。方法操作对象的内部状态,并作为对象间通信的主要机制”。因此,是“创建单个对象的蓝图”。

类的结构通常包括实例字段、构造函数和方法:

  • 实例字段将值作为实例化类的本地状态保存。
  • 构造函数是一种方法,它接收类实例化所需的参数,并利用这些参数返回类的实例(“对象”)。
  • 方法是通常用于操作实例化类数据的函数。方法通常有两种形式:公共方法和私有方法:
    • 公共方法是可由利用该对象的其他代码在实例化类之外调用的函数。
    • 私有方法,顾名思义,不能在实例化类之外调用。
    • 私有方法被公共方法甚至其他私有方法利用来完成实现细节与使用实例化类的代码无关的任务。

下面是一个简单的示例,展示了如何使用类,该示例突出了类的许多吸引程序员的方面。类明确区分了使用它的代码需要注意的内容和不需要注意的内容。类内置了编写代码、存储数据、操作数据和公开数据的机制。它易于复制——只需使用new关键字即可随心所欲地创建该类的另一个实例。它甚至还支持更高级的技术,例如继承,这可以减少执行相似但不同任务所需的工作量,但也有其缺点

class Book {
 // declare the values you want to store as part of this class
 author;
 title;
 readCount;

 // declare how the instance fields will get initialized
 constructor(author, title) {
   this.author = author;
   this.title = title;
   this.readCount = 0;
 }

 // create a private method that abstracts an implementation detail for use by a public method
 incrementReadCount() {
   this.readCount += 1;
 }

 // create a public method that can be executed outside of the class 
read() {
   console.log('This is a good book!');
   this.incrementReadCount();
 }

 getReadCount() {
   return this.readCount;
 }
}
Enter fullscreen mode Exit fullscreen mode
// somewhere in a different code block

const myFirstBook = new Book('Jack Beaton', 'Getting To Mars');
myFirstBook.getReadCount(); // 0
myFirstBook.read(); // 'This is a good book!'
myFirstBook.incrementReadCount(); // calling private methods outside the class won't work in strict OOP languages
myFirstBook.read(); // 'This is a good book!'
myFirstBook.readCount; // directly accessing a class's private fields won't work in strict OOP languages
myFirstBook.getReadCount(); // 2
Enter fullscreen mode Exit fullscreen mode

类看起来很酷!那么在 JS 中使用它们有什么不好呢?

在 JS 中使用这种模式本身并没有什么不好。然而,我经常遇到的问题是,这有时是 JS 代码库中唯一的模式,或者经常被误用。在决定使用哪种模式时,团队应该选择最能解决所用语言中问题的模式。但是,如果类的 OOP 模式是你唯一了解或喜欢的编码方式,那么当然,有时使用它会使代码更难理解,因为这种模式与问题和/或编程语言并不契合。

一只试图挤进容器里的猫

那么 JS 中的类的替代品是什么?

当然是JS 模块

什么是 JS 模块?

在 JavaScript 中,模块是一个导出可在其他地方使用的功能(例如变量、对象和函数)的文件。这些功能使用导出语法定义,模块内的代码将具有对其定义文件中声明的所有变量的作用域访问权限。导出的功能随后可以导入到任何其他文件或模块中。但模块外部的代码将无法访问模块中未导出的代码。

导出的变量可以是任何形式,包括类,只要它是有效的 JS 变量、函数或对象即可。实际上,这意味着在给定的文件中,我们可能在数据结构之外声明变量,但仍在数据结构内使用它们。这称为闭包。在其他语言中这是不允许的,但 JS 对闭包提供了强大的支持。

// some js file

const a = 1;
const b = 2;

export function add() {
 return a + b;
}
Enter fullscreen mode Exit fullscreen mode
// some other js file that imports the add() function

add(); // 3
Enter fullscreen mode Exit fullscreen mode

那么我们如何使用 JS 模块而不是类来编写 Book 数据结构呢?

像这样:

// some js file

// the exported function is what other modules will have access to
export function createBook(authorName, bookTitle) {
 // the following variables and functions are declared within the scope of the createBook function so other book instances or code cannot access these variables
 const author = authorName;
 const title = bookTitle;
 let readCount = 0;

 function incrementReadCount() {
   readCount += 1;
 }

 function read() {
   console.log('This is a good book!');
   incrementReadCount();
 }

 function getReadCount() {
   return readCount;
 }

 // only the methods listed as key-value pairs can be accessed by the returned Object
 return {
   read,
   getReadCount,
 };
}
Enter fullscreen mode Exit fullscreen mode
// in some other file

const mySecondBook = createBook('Gabriel Rumbaut', 'Cats Are Better Than Dogs');
mySecondBook.getReadCount(); // 0
mySecondBook.read(); // 'This is a good book!'
mySecondBook.incrementReadCount(); // will throw an error
mySecondBook.read(); // 'This is a good book!'
mySecondBook.readCount; // will also throw an error
mySecondBook.getReadCount(); // 2
Enter fullscreen mode Exit fullscreen mode

如你所见,我们喜欢的类的所有特性都融入到了我们的模块中,无需使用类的语法。由于实例字段和方法被替换为简单的变量,因此样板代码更少,并且它们的公共或私有状态由它们在导出createBook函数模块的返回值中包含或排除来表示。通过导出一个显式返回对象的函数,我们可以new完全摒弃类的语法。

但是依赖注入怎么样?

确实,很多人喜欢在 JS 中使用类,因为构造函数可以轻松实现依赖注入。我们将依赖项传递给新对象,而无需进行硬编码并将其与类耦合。例如,如果我的类需要调用数据库,我可以配置构造函数来接收数据库实例。这样做也很好,但请记住,JS 原生允许导入在其他地方创建的模块!因此,如果您想访问该数据库实例,请将其导出为模块,然后在实际需要的模块中导入。无需类,即可获得依赖注入的所有好处!

// old: db created or imported here and passed into Class
const library = new Library(db);

// new: db created or imported here and passed into Module
const library = createLibrary(db);
Enter fullscreen mode Exit fullscreen mode

此外,您可能不需要依赖注入。除非您要使用不同的数据库实例构建不同的库实例,否则将数据库直接导入到托管库类/函数的模块中,可以避免在多个文件中编写冗余代码。如果您预计不需要依赖注入,则无需特意去支持它。

// alternate #1: db imported within the Class
const library = new Library();

// alternate #2: db imported into Module
const library = createLibrary();
Enter fullscreen mode Exit fullscreen mode

如果我可以使用模块完成所有事情,为什么还需要类?

在 JavaScript 中,你不需要!你可以编写任何你想要的程序,而无需使用类或this关键字!事实上,类语法对 JavaScript 来说还是比较新的,面向对象代码之前都是用函数编写的。类语法只是基于函数的面向对象编程方法的语法糖。

那么为什么我这么频繁地看到课程呢?

因为在 ES5 之前,大多数人并不把 JS 当作一门严肃的语言。JS 引入 class 语法是有意为之,旨在吸引经验丰富的程序员,降低入门门槛,让 JS 更受欢迎。这个方法效果很好,但代价不菲。由于大多数人是在学习了 Java、C++ 甚至 Python 或 Ruby 等面向对象编程 (OOP) 语言之后才开始学习 JS 的,因此用 JS 语法重写他们已知的内容会更容易。当新手想从头开始学习 JS 时,他们可以参考这些面向对象编程 (OOP) 的 JS 代码,作为如何实现常见结构的真实示例,所以他们现在也算是面向对象编程 (OOP) 程序员了

我感觉你实际上并不认为任何人都应该在 JS 中使用类。

是什么泄露了秘密😃? Kyle Simpson比我更详细地解释了原因,JS 中的类不像其他语言那样是“一等公民”。大多数人不知道这一点,但在本质上,所有类都是函数,所有函数都是对象。(数组也是对象,但这不是重点。)因此,在大多数情况下,直接编写对象和函数会更好,因为这样行为上更一致,维护起来更愉快,编写起来更轻松,也更容易阅读。这样,当你的同事看到你的代码时,就不会像《富家穷路》里的约翰尼那样感到困惑。

被递来的文件弄糊涂的人

所以,JS 中的类是一种选择,而不是一种生活方式。我明白了。那么,您认为我什么时候应该使用它们,什么时候应该避免使用它们呢?

在以下情况下考虑使用类:

  • 大多数团队不熟悉模块
  • 你编写的代码不应该是惯用的 JS 代码的典范
  • 您希望利用在类范式中严格执行的广为人知的模式(例如,本地状态、公共/私有等)
  • 您计划多次实例化给定的类
  • 你并不总是用相同的参数来实例化类
  • 您计划利用类的全部或大部分功能(即继承、依赖注入等)

在以下情况下,请考虑避免上课:

  • 在给定的运行时中只能实例化一次您的类
  • 您的数据结构不需要任何本地状态
  • 您有最少的公共方法
  • 你没有扩展你的数据结构来创建新的数据结构
  • 您的构造函数仅用于依赖注入
  • 你的构造函数总是用相同的参数调用
  • 你想避免使用this

但是我喜欢在 JS 中使用类。你真的认为我应该放弃它们吗?

是的!但更重要的是,我只想让你知道,你还有其他选择。不仅在编程方面,在生活中也是如此。如果你挑战关于你做事方式的基本假设,你会发现和学到的东西,远比你继续一成不变要多得多。

如果您仍然坚持使用类,或者受到一些限制,导致您无法正确实现模块,那也没关系。如果您运气不错,TC39将在 JS 的下一个版本ES2022中添加public/private/static语法。但请务必了解您的决定可能带来的影响,尤其是在您编写的类没有充分利用类的所有优势的情况下!

有关可用于 JavaScript 编码的所有不同类型的模式的更多信息,请参阅Addy Osmani 的《学习 JavaScript 设计模式》

鏂囩珷鏉ユ簮锛�https://dev.to/jaraujo6/stop-using-javascript-classes-33ij
PREV
在 React 中创建可重用的网格系统
NEXT
使用 Playwright 进行行为驱动开发 (BDD)