JS基础与高级应用: 函数式编程

550 阅读9分钟

函数式编程是一种以函数为核心的编程范式,强调通过纯函数和无副作用来实现程序逻辑,具有高可读性、可维护性和扩展性。本文探讨了函数式编程的出现与发展历程,比较了命令式编程和函数式编程在解决URL参数解析问题上的差异,详细介绍了函数式编程的原理和特点,包括函数的一等公民地位、声明式编程风格、柯里化、纯函数、惰性函数以及函数组合。文章还通过示例代码展示了如何实现一个支持链式调用和参数累加的柯里化函数,进一步阐明了函数式编程的优势和应用。

函数式编程

函数式编程的出现和发展历程

函数式编程是一种编程范式,其发展经历了多个阶段,从命令式编程到面向对象编程,再到函数式编程。每个阶段都旨在提高代码的可读性、可维护性和扩展性。

发展历程

  • 命令式编程(Imperative Programming)
    • 强调“怎么做”(How to do it)
    • 通过一系列步骤和状态变化来实现程序逻辑
  • 面向对象编程(Object-Oriented Programming, OOP)
    • 强调“做什么”(What to do)
    • 通过对象和类来组织代码,封装数据和行为
  • 函数式编程(Functional Programming, FP)
    • 强调“做什么”(What to do)和“为什么”(Why)
    • 通过纯函数和无副作用来实现程序逻辑,鼓励不可变性和函数组合

code

一道面试题理解命令式和函数式:浏览器URL参数解析数组

解析URL参数为数组是一个常见的问题,我们将从命令式和函数式两种编程风格来解决这个问题。

命令式编程解决方案

  1. 分析数组在URL中的展现形式
    • URL: http://example.com?param[]=1&param[]=2&param[]=3
  2. 参数提取和拼接数组
    • 使用正则表达式或字符串操作来提取参数
  3. 转换成数组对象
    • 使用 split 方法将字符串转换为数组
  4. 代码实现

code

存在问题

  • 过程存在大量包裹逻辑(高耦合),难以理解
  • 存在临时变量,扩展性差

函数式解决方案

  1. 需求分析
    • 将数组字符串转换为数组对象 [字符串 => 对象]
    • nameParse => [objectHelper :: string > object]
  2. 功能明确
    • objectHelper 负责将字符串转换为对象,包含 formatNameassembleObj
  3. 功能拆分
    • objectHelper = [(split + capitalize + join)] + assembleObj(原子函数,不可拆分)
  4. 代码实现

code

原则:每一个函数都拆分成原子函数

  • 什么是原子函数?
    • 无副作用的函数,只依赖输入参数并返回输出结果。

通过对比命令式和函数式编程风格的URL参数解析方法,可以看出函数式编程通过原子函数和组合函数的使用,使代码更清晰、更易读、更易维护和扩展。这种编程风格鼓励使用小而纯的函数来构建复杂的逻辑,从而提高代码质量和开发效率。

函数式编程的原理和特点

函数式编程(Functional Programming, FP)是一种编程范式,它以函数为核心,通过组合和应用函数来实现程序逻辑。其核心思想源于数学的函数概念和计算机科学中的λ演算。下面将详细探讨函数式编程的原理和特点。

函数式编程的原理

函数式编程的原理可以通过数学中的概念来理解,比如加法的结合律和因式分解。举个例子,加法结合律说明 (a + b) + c = a + (b + c),这一性质在函数组合中也类似。

理论思想

函数式编程的思想与传统的命令式编程有很大不同。以下是几个关键点:

a. 函数是一等公民

在函数式编程中,函数被视为一等公民。这意味着函数可以像其他数据类型一样被传递、返回和存储。函数式编程的核心在于通过原子函数和组合函数来实现逻辑功能:

  • 原子函数:最小的功能单元,只实现单一功能。
  • 函数组合:将多个原子函数拼接成复杂的逻辑。
b. 声明式编程

函数式编程又称为声明式编程。它关注的是“做什么”而不是“怎么做”。开发者通过声明需求来实现逻辑,而不是详细描述实现步骤。例如,Vue 3 的 Composition API 和 React 的 Hook 都是函数式编程思想的体现,通过组合基础函数来实现复杂功能。

c. 传参统一与柯里化

函数式编程中,参数传递通常被统一为单个参数的传递,这个过程称为柯里化(Currying)。柯里化将一个多参数函数转换为多个单参数函数的嵌套调用。这不仅简化了参数处理,还增强了函数的可组合性。

d. 纯函数

纯函数是函数式编程的核心概念之一。纯函数具有以下特征:

  • 无副作用:函数的执行不会影响外部状态或改变全局变量。
  • 确定性:对于相同的输入,函数总是返回相同的输出。

纯函数的这种特性使得它们更容易测试和推理,从而提高了代码的可靠性和可维护性。

3. 惰性函数

惰性函数是一种延迟计算的技术,它先装载函数,然后在需要时再执行。类似于面向对象编程中函数的重载。这种技术常用于高阶函数(Higher-Order Functions, HOC),比如在React中,HOC是一种用于增强组件功能的模式。

code

4. 无状态与无副作用的纯函数

纯函数的无状态和无副作用特性使其特别适合函数式编程:

  • 无状态:函数的执行不依赖于外部状态,也不会改变外部状态。函数的输出仅取决于输入参数。
  • 无副作用:函数内部不能对系统的其他部分产生影响,如修改全局变量或I/O操作。

通过无状态和无副作用,纯函数实现了幂等性,即多次调用函数不会改变程序的行为。这使得函数更具可预测性和可测试性。

code

5. 流水线的组装:加工与组装

函数式编程中,函数的组合类似于流水线的加工和组装:

a. 加工 - 柯里化

柯里化将多参数函数转化为一系列单参数函数。例如,将 f(x, y, z) 转化为 f(x)(y)(z)。这种方式简化了参数收集和函数组合的过程。

通过问题理解柯里化: 手写构造可拆分的传参累加函数

你需要实现一个函数 add,它可以通过链式调用(也就是连续多次调用)来累加传入的参数,并且在最终调用时返回累加的结果。例如:

const total = +add(1)(2)(3)(4); console.log(total); // 输出 10

要求

  1. 柯里化结构:函数 add 应该支持无限次的链式调用,每次调用都接受一个或多个参数。

  2. 参数收集:每次链式调用时,将传入的参数收集起来。

  3. 类数组处理:在函数内部处理传入的类数组 arguments。

    你可以使用以下三种方式之一:

    • 展开运算符(...
    • Array.prototype.slice.call
    • Array.from
  4. 递归返回:在每次链式调用时,函数应返回自身(递归),以便继续收集参数。

  5. 累加功能:实现一个功能区,用于累加所有收集到的参数。

  6. 输出结果:在最终调用时,返回所有参数的累加和。可以通过重写 toString 方法或者其他方式实现。

code

b. 组装 - 流水线

函数组合类似于流水线上的组装。一个函数的输出可以作为下一个函数的输入,通过函数的组合可以实现复杂的逻辑。一个典型的函数组合例子是:

const compose = (f, g) => x => f(g(x));

这种组合方式使得函数的执行过程更为直观和清晰。

code

在函数式编程中,函数组合的组装可以通过多种形式来实现。主要有 composepipe 两种形式。

compose 是一种从右到左组合函数的方式。它将多个函数组合成一个函数,并依次执行每个函数,前一个函数的输出作为后一个函数的输入。

const compose = (f, g) => x => f(g(x));

const add5 = x => x + 5;
const multiply3 = x => x * 3;

const add5ThenMultiply3 = compose(multiply3, add5);

console.log(add5ThenMultiply3(2)); // 输出 21

pipe 是一种从左到右组合函数的方式。它将多个函数组合成一个函数,并依次执行每个函数,前一个函数的输出作为后一个函数的输入。与 compose 的顺序相反,pipe 更符合直觉的执行顺序。

const pipe = (...functions) => input => functions.reduce((acc, fn) => fn(acc), input);

const add5 = x => x + 5;
const multiply3 = x => x * 3;

const add5ThenMultiply3 = pipe(add5, multiply3);

console.log(add5ThenMultiply3(2)); // 输出 21

组合多函数

无论使用 compose 还是 pipe,都可以组合多个函数,从而实现复杂的功能。

const compose = (...functions) => input => functions.reduceRight((acc, fn) => fn(acc), input);
const pipe = (...functions) => input => functions.reduce((acc, fn) => fn(acc), input);

const add = x => x + 1;
const multiply = x => x * 2;
const subtract = x => x - 3;

const composedFunction = compose(subtract, multiply, add);
const pipedFunction = pipe(add, multiply, subtract);

console.log(composedFunction(5)); // 输出 9
console.log(pipedFunction(5));    // 输出 9

总结

函数式编程通过将函数视为一等公民,强调纯函数和无副作用,实现了代码的高可读性、可测试性和可维护性。它的核心理念包括声明式编程、柯里化、纯函数以及函数组合,提供了一种不同于命令式编程的思考方式。通过对比命令式和函数式解决方案的示例,展示了函数式编程如何通过原子函数和函数组合实现复杂逻辑,减少代码耦合,提高扩展性。函数式编程不仅在理论上具有坚实的基础,也在实践中展现了其优越性,成为现代软件开发中不可忽视的重要工具。