函数式 vs 面向对象编程在JS中:2024年如何选择?

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

一枚硬币的两面:JavaScript中的函数式与面向对象方法

\n\n

如果你曾经思考过,为什么有些开发者写的代码看起来像数学公式,而另一些人的代码则像宜家家具的组装说明书,那么你就遇到了一个永恒的争论:JavaScript中的函数式编程(FP)与面向对象编程(OOP)。

\n\n

JavaScript是一种独特的语言。它最初是作为按钮的脚本语言诞生的,但后来成长为无所不能的巨兽。它是多范式的:你可以用过程式、面向对象或函数式风格编写代码。这就是最有趣的地方。新手应该选择哪种方法?有经验的开发者呢?让我们不带偏见,通过实际例子来弄清楚。

\n\n

简要概述:什么是面向对象和函数式编程?

\n\n

面向对象方法(OOP)是关于“实体”的。你有对象(猫、汽车、用户),它们有属性(颜色、速度、姓名)和方法(喵喵叫、行驶、登录)。你将数据和行为组合在一起。经典例子:class Car { drive() {} }。

\n\n

函数式方法(FP)是关于“动作”的。数据和函数是分离的。函数是纯转换器:输入一些数据,输出另一些数据,没有副作用。没有this、突变或隐藏状态。

\n\n

讽刺的是,JavaScript在“开箱即用”的情况下对两者都不完美,但它允许优雅地使用两种风格。问题只在于何时应用哪一种。

\n\n

JavaScript中的OOP:从原型到ES6类

\n\n

很久以前,在IE6时代,JS中的OOP看起来令人望而生畏。一切都建立在原型之上——一种对象从其他对象继承属性的机制。这很强大,但不直观。

\n\n

随着ES6(ES2015)的到来,语言中添加了语法糖——class。但重要的是要理解:在底层,它仍然是相同的原型。类使代码更清晰,更容易被来自Java或C++的人理解。

\n\n

OOP示例:“用户”类

\n\n
class User {\n  constructor(name, email) {\n    this.name = name;\n    this.email = email;\n    this.loggedIn = false;\n  }\n\n  login() {\n    this.loggedIn = true;\n    return `${this.name} 已登录系统`;\n  }\n\n  logout() {\n    this.loggedIn = false;\n    return `${this.name} 已退出`;\n  }\n}\n\nconst admin = new User('安娜', 'anna@example.com');\nconsole.log(admin.login()); // 安娜 已登录系统\n
\n\n

一切都很合理:数据(name、email)与方法(login、logout)共存。对象的状态通过其自身的方法改变。这是经典的封装。

\n\n

JS中OOP的问题

\n
    \n
  • this上下文。上下文丢失是新手的痛点。如果将方法作为回调传递,this会“失效”。必须使用.bind()、箭头函数或闭包。
  • \n
  • 继承。深层类层次结构(“菱形问题”)会导致混乱。通常组合(mixins)比继承更好。
  • \n
  • 状态。可变状态(当我们改变对象属性时)是大型项目中bug的主要来源。一个方法可能意外地改变它不应该改变的东西。
  • \n
\n\n

函数式方法:纯净与可预测性

\n\n

函数式程序员对OOP略带嘲讽。他们的座右铭是:“数据就是数据,函数就是函数。不要把它们混在一起。”

\n\n

JavaScript中函数式编程的关键概念:

\n
    \n
  • 纯函数——结果仅取决于参数,没有副作用。
  • \n
  • 不可变性(Immutability)——你不改变数据,而是创建它们的新副本。
  • \n
  • 高阶函数——接受函数作为参数或返回函数的函数。
  • \n
  • 闭包——函数记住其创建时的作用域。
  • \n
\n\n

函数式示例:用户操作

\n\n
// 纯函数:创建用户\nconst createUser = (name, email) => ({\n  name,\n  email,\n  loggedIn: false\n});\n\n// 纯函数:登录(返回新对象,不改变原对象)\nconst login = (user) => ({\n  ...user,\n  loggedIn: true\n});\n\n// 纯函数:退出\nconst logout = (user) => ({\n  ...user,\n  loggedIn: false\n});\n\nconst user = createUser('伊万', 'ivan@example.com');\nconst loggedInUser = login(user);\nconsole.log(loggedInUser.name); // 伊万\nconsole.log(user.loggedIn); // false — 原始对象未改变\n
\n\n

注意:没有class,没有this,没有突变。每个函数只做一件事,并返回新数据。这种代码更容易测试和调试。

\n\n

JS中函数式方法的问题

\n
    \n
  • 性能。创建大量数据副本(特别是数组和对象)可能比原地修改慢。在大型数据集上,这可能成为瓶颈。
  • \n
  • 可读性。对于来自OOP背景的开发者来说,函数式代码可能看起来不寻常。大量的reduce、map和filter链有时难以理解。
  • \n
  • 与库的交互。许多流行的库(如Express、Mongoose)是面向对象的。在函数式风格中使用它们需要额外的包装器。
  • \n
\n\n

那么2024年选择什么?

\n\n

简短的回答:两者都选。JavaScript的美妙之处在于你不必局限于一种范式。现代开发越来越多地使用混合方法:

\n\n
    \n
  • 使用函数式方法处理数据转换、业务逻辑和状态管理(Redux、React hooks)。
  • \n
  • 使用OOP处理UI组件、服务、控制器和需要封装状态的实体。
  • \n
\n\n

例如,在React中,组件本身可以是类(OOP)或函数(函数式)。但现代React推荐函数式组件配合hooks——这本质上是函数式方法,但带有一些OOP元素(如useState中的本地状态)。

\n\n

何时优先选择OOP:

\n
    \n
  • 你正在构建具有复杂状态的大型应用程序(游戏、图形编辑器)。
  • \n
  • 你使用具有类的框架(Angular、NestJS)。
  • \n
  • 你的团队来自Java/C#背景。
  • \n
  • 你需要多态性(同一接口的不同实现)。
  • \n
\n\n

何时优先选择函数式编程:

\n
    \n
  • 你处理数据流(数据管道、ETL)。
  • \n
  • 你使用Redux或类似的状态管理。
  • \n
  • 你编写需要易于测试的代码(纯函数易于测试)。
  • \n
  • 你处理异步操作(Promise链、async/await与函数式组合配合良好)。
  • \n
  • 你重视代码的可预测性和最小化副作用。
  • \n
\n\n

结论:没有圣战

\n\n

函数式编程与面向对象编程的争论就像吉他手与贝斯手的争论:它们听起来不同,但在一起时效果最好。JavaScript让你可以同时使用两者,而不必牺牲理智。

\n\n

主要建议:不要试图成为纯粹主义者。学习两种范式,理解它们的优缺点,并根据任务选择工具。2024年,一个优秀的开发者是能够根据上下文在范式之间切换的人。

\n\n

记住:代码是为人而写的,而不是为机器。选择让你的代码更清晰、更易于维护的方法。

博客

书籍推荐