陌上人如玉
公子世无双

JavaScript 中的双峰并峙:函数式编程 vs 面向对象编程

作为一门多范式编程语言,JavaScript 同时支持函数式编程(FP)和面向对象编程(OOP)两种核心编程范式。这两种范式并非对立关系,而是各有侧重、可互补使用的编程思想。本文将深入解析这两种编程范式的核心思想、实现方式,并通过实例对比其应用场景。

一、面向对象编程(OOP):以”对象”为中心的世界

1. 核心思想

面向对象编程以对象为基本单元,将数据(属性)和操作数据的方法封装在一起,通过封装、继承、多态三大特性组织代码,模拟现实世界的实体和交互。

在 JavaScript 中,OOP 基于原型(Prototype)实现,ES6 引入的 class 语法糖让 OOP 写法更贴近传统面向对象语言(如 Java)。

2. 核心特性解析

(1)封装:隐藏内部实现,暴露公共接口

封装的目的是将对象的状态和行为绑定,同时控制外部对内部数据的访问权限。

// ES6 class 实现封装
class User {
  // 构造函数:初始化对象属性
  constructor(name, age) {
    this.name = name; // 公共属性
    this._age = age;  // 约定俗成的私有属性(下划线命名)
  }

  // 公共方法:暴露给外部的接口
  getInfo() {
    return `姓名:${this.name},年龄:${this._age}`;
  }

  // 访问器方法:控制属性的读取和修改
  get age() {
    return this._age;
  }

  set age(newAge) {
    if (newAge >= 0 && newAge <= 120) {
      this._age = newAge;
    } else {
      throw new Error('年龄必须在 0-120 之间');
    }
  }
}

// 使用示例
const user = new User('张三', 25);
console.log(user.getInfo()); // 姓名:张三,年龄:25
user.age = 30; // 通过 setter 修改属性
console.log(user.age); // 30
user.age = 150; // 抛出错误:年龄必须在 0-120 之间

(2)继承:复用已有类的属性和方法

继承允许子类复用父类的代码,同时可扩展自身特性。JavaScript 中通过原型链实现继承,ES6 的 extends 简化了继承写法。

// 父类
class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(`${this.name} 正在进食`);
  }
}

// 子类继承父类
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  // 重写父类方法(多态)
  eat() {
    console.log(`${this.name}(${this.breed})正在吃狗粮`);
  }

  // 子类扩展方法
  bark() {
    console.log(`${this.name} 汪汪叫`);
  }
}

// 使用示例
const goldenRetriever = new Dog('旺财', '金毛');
goldenRetriever.eat(); // 旺财(金毛)正在吃狗粮
goldenRetriever.bark(); // 旺财 汪汪叫

(3)多态:同一行为的不同表现形式

多态允许不同对象对同一方法做出不同响应,核心是”重写”和”重载”(JavaScript 无原生重载,可通过参数处理模拟)。上面的 Dog.eat() 就是典型的多态实现。

3. OOP 典型应用场景

  • 模拟现实世界实体(如用户、订单、商品)
  • 大型应用的模块划分(如组件化开发)
  • 需要维护状态的场景(如表单对象、游戏角色)

二、函数式编程(FP):以”函数”为核心的思维

1. 核心思想

函数式编程将计算视为函数的组合,强调纯函数、不可变数据、无副作用,避免状态修改和可变数据,核心是”做什么”而非”怎么做”。

JavaScript 中函数是一等公民(可作为参数、返回值、赋值给变量),这为函数式编程提供了天然支持。

2. 核心概念解析

(1)纯函数:无副作用的函数

纯函数满足两个条件:
– 相同输入始终返回相同输出(无外部状态依赖)
– 不修改外部状态(无副作用,如修改全局变量、DOM、网络请求)

// 纯函数:仅依赖输入,无副作用
function add(a, b) {
  return a + b;
}

// 非纯函数:依赖外部变量,结果不可预测
let base = 10;
function addWithBase(num) {
  return num + base;
}

// 非纯函数:修改外部状态(副作用)
function updateUserAge(user, newAge) {
  user.age = newAge; // 修改了传入的对象
  return user;
}

// 纯函数替代方案:返回新对象,不修改原对象
function updateUserAgePure(user, newAge) {
  return { ...user, age: newAge }; // 解构创建新对象
}

// 使用示例
const user = { name: '李四', age: 28 };
const newUser = updateUserAgePure(user, 29);
console.log(user.age); // 28(原对象未变)
console.log(newUser.age); // 29(新对象)

(2)不可变数据:数据一旦创建就不可修改

JavaScript 中原始类型(字符串、数字、布尔)本身不可变,引用类型(对象、数组)需通过创建新值实现不可变。

// 错误:直接修改数组(可变)
const arr = [1, 2, 3];
arr.push(4); // 修改原数组

// 正确:创建新数组(不可变)
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // 解构创建新数组
console.log(arr); // [1,2,3]
console.log(newArr); // [1,2,3,4]

// 对象不可变示例
const obj = { a: 1 };
const newObj = { ...obj, b: 2 }; // 解构创建新对象

(3)高阶函数:操作函数的函数

高阶函数满足以下任一条件:
– 接收一个或多个函数作为参数
– 返回一个新函数

JavaScript 内置的 mapfilterreduce 都是典型的高阶函数:

// 示例:使用高阶函数处理数组
const numbers = [1, 2, 3, 4, 5];

// map:遍历数组,返回新数组(纯函数)
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2,4,6,8,10]

// filter:过滤数组,返回新数组(纯函数)
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2,4]

// reduce:归约数组,返回单一值(纯函数)
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15

// 自定义高阶函数
function withLog(fn) {
  return function(...args) {
    console.log(`调用函数,参数:${args.join(',')}`);
    const result = fn(...args);
    console.log(`函数返回值:${result}`);
    return result;
  };
}

// 使用自定义高阶函数
const loggedAdd = withLog(add);
loggedAdd(2, 3); 
// 输出:
// 调用函数,参数:2,3
// 函数返回值:5

(4)函数组合:将多个函数组合成新函数

函数组合是函数式编程的核心,将多个简单函数组合成复杂逻辑:

// 定义基础函数
const add1 = num => num + 1;
const multiply2 = num => num * 2;
const square = num => num * num;

// 函数组合:从右到左执行
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);

// 组合新函数:先加1,再乘2,最后平方
const calculate = compose(square, multiply2, add1);

// 使用示例
console.log(calculate(3)); // ((3+1)*2)^2 = 64

3. FP 典型应用场景

  • 数据处理和转换(如数组过滤、映射、归约)
  • 异步编程(如 Promise、async/await 本质是函数式思想)
  • 无状态的业务逻辑(如工具函数、计算函数)
  • 高并发、高可靠性场景(无状态易并行)

三、两种范式的对比与融合

1. 核心差异对比

维度 面向对象编程(OOP) 函数式编程(FP)
核心单元 对象(数据+方法) 函数(纯函数、高阶函数)
状态管理 允许状态可变(对象属性) 强调状态不可变(纯函数)
代码组织 封装、继承、多态 函数组合、柯里化、管道
关注点 怎么做(过程) 做什么(结果)
副作用 允许(如修改对象属性) 避免(纯函数无副作用)

2. 并非对立:JavaScript 中的范式融合

在实际开发中,纯 OOP 或纯 FP 都很少见,更多是混合使用:

// 融合示例:面向对象封装 + 函数式处理
class ShoppingCart {
  constructor() {
    this.items = []; // 内部状态
  }

  // 纯函数:计算总价(无副作用)
  calculateTotal() {
    return this.items.reduce((total, item) => total + item.price * item.quantity, 0);
  }

  // 函数式思想:添加商品时返回新状态(不可变)
  addItem(item) {
    this.items = [...this.items, item]; // 创建新数组,不直接修改原数组
    return this;
  }

  // 高阶函数:过滤商品
  filterItems(predicate) {
    return this.items.filter(predicate);
  }
}

// 使用示例
const cart = new ShoppingCart();
cart.addItem({ name: '手机', price: 2999, quantity: 1 });
cart.addItem({ name: '耳机', price: 199, quantity: 2 });

console.log(cart.calculateTotal()); // 2999 + 199*2 = 3397
const expensiveItems = cart.filterItems(item => item.price > 1000);
console.log(expensiveItems); // [{ name: '手机', price: 2999, quantity: 1 }]

四、如何选择合适的范式?

  • 选 OOP:需要模拟实体、维护状态、强调模块化和复用(如 UI 组件、业务实体)
  • 选 FP:需要数据处理、无状态逻辑、强调可预测性和可测试性(如工具函数、数据转换)
  • 混合使用:大多数业务场景,用 OOP 封装实体和状态,用 FP 处理数据和逻辑

总结

  1. 面向对象编程以对象为核心,通过封装、继承、多态组织代码,适合模拟实体和维护状态;
  2. 函数式编程以纯函数为核心,强调不可变数据和无副作用,适合数据处理和无状态逻辑;
  3. JavaScript 作为多范式语言,无需拘泥于单一范式,应根据场景灵活融合 OOP 和 FP 的优势,写出更优雅、可维护的代码。

掌握两种范式的核心思想,不仅能提升代码质量,更能拓宽编程思维——这也是成为优秀 JavaScript 开发者的关键。

赞(0) 打赏
未经允许不得转载:陌上寒 » JavaScript 中的双峰并峙:函数式编程 vs 面向对象编程

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫