JavaScript函数式编程

196 阅读38分钟

从入门到出门沉浸式带你领略JavaScript函数式编程,深入了解高阶函数、柯里化、尾递归优化和函子等深奥晦涩的知识。

函数式编程概述

函数式编程强调使用函数进行计算,避免使用可变状态和副作用。它依赖于数学函数的概念,即相同的输入始终会产生相同的输出

编程范式

函数式编程(Functional Programming, FP),FP是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

JavaScript支持多种编程范式,包括命令式、面向对象和函数式编程。

常见编程范式

面向过程编程(Procedural Programming)
  • 定义通过 过程或函数 来组织代码,强调任务的步骤。如C语言。

  • 特点

    • 将程序分解为函数和过程。
    • 数据与操作分开,数据通常通过参数传递。
    • 代码可重复使用,通过调用函数实现。
  • 优点

    • 简单易懂,适合小型项目。
    • 逻辑清晰,流程易于追踪。
  • 缺点

    • 难以管理大型项目,代码可读性降低。
    • 数据封装性差,容易导致错误。
面向对象编程(Object-Oriented Programming, OOP)
  • 定义通过 对象和类 来组织代码,强调数据与操作的封装。 如Java,C++,python。

  • 特点

    • 使用类定义对象的属性和方法。
    • 支持继承、多态和封装。
    • 数据和行为紧密结合,促进模块化。
  • 优点

    • 更好的代码复用和扩展性。
    • 适合大型系统开发,易于维护。
  • 缺点

    • 学习曲线较陡,设计复杂。
    • 可能导致性能开销。
函数式编程(Functional Programming)
  • 定义将计算视为 数学函数 的应用,强调 无状态 不可变性 如 Haskell、Lisp、JavaScript。

  • 特点

    • 使用高阶函数、纯函数和函数组合。
    • 避免使用可变状态,强调数据流。
    • 支持递归和惰性计算。
  • 优点

    • 更易于并行处理,减少副作用。
    • 提高代码的可测试性和可维护性。
  • 缺点

    • 对于初学者来说,概念较难理解。
    • 在某些情况下,性能可能不如命令式编程。

其他编程范式

  • 元编程(Metaprogramming)

    • 编写程序来操作或生成其他程序代码,通常用于代码生成和自动化。如 Ruby 的元编程特性、JS中的Reflect等。
    • 特点:代码生成、反射、编译时宏。
  • 声明式编程(Declarative Programming)

    • 通过描述 "做什么" 而不是 "怎么做",常用于数据库查询和 HTML。典型代表是 SQL、XQuery、React。
    • 特点:逻辑性、表达数据关系而非操作步骤。
  • 并发编程(Concurrent Programming)

    • 通过多线程或多进程并发执行多个任务,提高程序的性能和响应性。典型代表是 Java(线程)、Erlang(Actor 模型)。
    • 特点:线程管理、同步与异步、锁机制。
  • 逻辑编程(Logic Programming)

    • 基于逻辑推理,通过声明规则和事实,解决问题的解得自定义推理。典型代表是 Prolog。
    • 特点:使用逻辑规则、递归和模式匹配。
  • 面向事件编程(Event-Driven Programming)

    • 以事件的发生来驱动程序的执行,常用于 GUI 应用程序和服务器编程。典型代表是 JavaScript(浏览器环境)、Node.js。
    • 特点:事件监听器、回调函数。
  • 反应式编程(Reactive Programming)

    • 强调数据流和变化的传播,通过响应数据变化来驱动程序逻辑。典型代表是 RxJava、ReactiveX。
    • 特点:数据流、观察者模式、异步事件流。
  • 领域特定语言(Domain-Specific Languages, DSL)

    • 专注于特定领域的问题,通过设计特定的语言或语法来解决特定领域的需求。典型代表是 SQL、正则表达式、MATLAB。
    • 特点:专注于某一领域的表达能力、简化领域问题的表达。

函数式编程优点

  • 可预测性:因为函数是纯粹的,给定相同的输入,它们总是会产生相同的输出。
  • 更容易调试和测试:由于函数没有副作用,因此函数的行为是可预测的,更容易单元测试。
  • 代码简洁和可组合性强:函数可以通过组合来创建复杂的操作。

举个栗子:

  • 非函数式:
const num1 = 2
const num2 = 3
const sum = num1 + num2
console.log(sum)
// 5
  • 函数式:
function add (n1, n2) {
  return n1 + n2
}
const sum = add(2, 3)
console.log(sum)
// 5

必知基本概念

纯函数

纯函数(Pure Function) 是指满足以下两个条件的函数:

  • 确定性:纯函数对于相同的输入,总是返回相同的输出。
  • 无副作用:纯函数不会改变外部状态,也不会依赖外部的可变状态。

纯函数的特点

  • 无副作用:函数不依赖也不修改函数外部的状态,不会影响或依赖外部的任何变量、数据结构、数据库等。副作用通常包括修改全局变量、改变传入的对象、I/O 操作等。
  • 可缓存性 (Memoization) :由于纯函数总是返回相同的输出,因此可以对纯函数的输出进行缓存,从而在将来相同的输入下直接返回缓存的结果,提高效率。
  • 易测试性:纯函数只依赖输入参数进行计算,测试时无需设置复杂的环境或模拟外部依赖。
  • 并发和并行编程的安全性:纯函数不依赖于共享状态,也不会修改外部状态,因此在并发或并行编程环境中可以安全地使用而不必担心数据竞争或竞态条件。
  • 可组合性:纯函数可以轻松组合在一起构建更复杂的函数。例如,函数组合(Function Composition)就是将多个纯函数组合成一个新函数。

纯函数的例子

function doubleArray(arr) {
  return arr.map(x => x * 2);
}
// 对于相同的数组输入,doubleArray总是返回相同的数组结果。
// doubleArray不会改变传入的arr数组,而是返回一个新的数组。

可缓存性的例子:记忆化

  1. 使用Lodash中的memoize 函数
// 引入 lodash
const _ = require('lodash');

// 定义一个计算开销大的函数
function expensiveCalculation(num) {
    console.log(`Calculating ${num}...`);
    return num * 2; // 示例计算
}

// 使用 lodash 的 memoize 函数进行记忆化
const memoizedCalculation = _.memoize(expensiveCalculation);

// 测试记忆化函数
console.log(memoizedCalculation(5)); // 计算并输出 10
console.log(memoizedCalculation(5)); // 输出 10(从缓存中获取)
console.log(memoizedCalculation(10)); // 计算并输出 20
  1. 自己动手搓一个
function memoize(fn) {
    const cache = new Map();

    return function(...args) {
        const key = JSON.stringify(args); // 使用参数作为缓存键

        if (cache.has(key)) {
            return cache.get(key); // 如果缓存中有结果,直接返回
        }

        const result = fn(...args); // 计算结果
        cache.set(key, result); // 存入缓存
        return result; // 返回计算结果
    };
}

// 示例函数
function expensiveCalculation(num) {
    console.log(`Calculating ${num}...`);
    return num * 2; // 示例计算
}

// 使用自定义的记忆化函数
const memoizedCalculation = memoize(expensiveCalculation);

// 测试记忆化函数
console.log(memoizedCalculation(5)); // 计算并输出 10
console.log(memoizedCalculation(5)); // 输出 10(从缓存中获取)
console.log(memoizedCalculation(10)); // 计算并输出 20

将非纯函数改造成纯函数

  1. 移除外部状态依赖:将外部状态作为参数传递给函数,而不是直接在函数内部引用外部变量。
  2. 避免修改传入的参数:如果需要改变数据结构,可以返回一个新的数据结构,而不是在原有数据结构上进行修改。
  3. 将I/O操作与计算逻辑分离:将副作用代码(如I/O操作)与核心计算逻辑分开处理。例如,可以将I/O操作放在函数外部,传入数据给纯函数进行处理。
// 非纯函数
function updateObject(obj, key, value) {
  obj[key] = value;
  return obj;
}

// 纯函数
function updateObjectPure(obj, key, value) {
  return { ...obj, [key]: value };
}
// 在非纯函数中,updateObject直接修改了传入的obj对象。
// 而在纯函数版本中,updateObjectPure返回一个新的对象,保留了原始对象的不可变性。

不可变性

不可变性(Immutability) 是指一旦创建的数据对象,其状态不可被改变。不可变的数据结构在修改时,实际上会返回一个新的数据结构,原数据结构保持不变

不可变性的优点

  1. 减少副作用不可变性保证了数据结构不会被修改,从而避免了因为数据的变化而产生的副作用。这使得函数变得更加可靠和可预测。
  2. 提高代码可读性和可维护性:不可变的数据结构简化了数据的跟踪和管理,使代码更容易理解和维护。因为数据结构的变化不影响原有的数据,所以逻辑更简单。
  3. 易于调试和测试:由于数据在函数调用期间不会改变,容易追踪数据的变化,从而简化了调试过程。纯函数和不可变数据结构结合起来,使得测试变得更容易。
  4. 支持并发编程:不可变数据结构在多线程或异步环境中特别有用,因为多个线程或异步操作可以安全地访问相同的数据而不需要锁定或同步机制。
  5. 数据结构版本控制:不可变性使得我们可以轻松实现数据结构的版本控制。通过维护不同版本的不可变数据结构,可以进行历史追溯和回滚操作。

不可变数据结构的实现

原生 JavaScript 对象和数组

在原生 JavaScript 中,对象和数组是可变的。对于需要不可变特性的应用,常用方法包括:

  • 对象赋值:使用对象展开运算符 (...) 创建新对象。
  • 数组操作:使用数组方法 (concat, slice, map, filter) 返回新数组。
const person = { name: 'Alice', age: 25 };

// 不可变更新
const updatedPerson = { ...person, age: 26 };
console.log(updatedPerson); // { name: 'Alice', age: 26 }
console.log(person); // { name: 'Alice', age: 25 }
const numbers = [1, 2, 3];

// 不可变添加元素
const newNumbers = [...numbers, 4];
console.log(newNumbers); // [1, 2, 3, 4]
console.log(numbers); // [1, 2, 3]
使用不可变数据结构库
Immutable.js

Immutable.js 提供了不可变的数据结构,如 ListMapSet 等,这些数据结构在修改时会返回新的实例,原始数据结构保持不变。

const { Map } = require('immutable');
const person = Map({ name: 'Alice', age: 25 });

// 不可变更新
const updatedPerson = person.set('age', 26);
console.log(updatedPerson.toJS()); // { name: 'Alice', age: 26 }
console.log(person.toJS()); // { name: 'Alice', age: 25 }
Immer

Immer 允许你以可变的方式编写代码,但会自动生成不可变的更新。它通过使用“草稿”来处理数据,简化了不可变数据结构的操作。

const produce = require('immer');

const person = { name: 'Alice', age: 25 };

const updatedPerson = produce(person, draft => {
  draft.age = 26;
});

console.log(updatedPerson); // { name: 'Alice', age: 26 }
console.log(person); // { name: 'Alice', age: 25 }

引用透明性

引用透明性(Referential Transparency) 是指一个表达式在程序的任何地方都可以被其值所替代,而不改变程序的行为。这意味着如果你将一个表达式替换成它的值,程序的结果和行为应当保持不变。

引用透明性的优点

  1. 可替代性: 如果一个表达式在程序中可以被它的计算结果所替代而不改变程序的结果,这个表达式就是引用透明的。换句话说,表达式的结果在所有上下文中都是一致的

  2. 简化代码理解: 由于引用透明性保证了表达式的一致性,代码的理解和维护变得更加容易。你可以在代码的任意位置替换表达式而不会影响程序的行为

  3. 优化和重构的支持: 引用透明性使得代码的优化和重构变得更加安全和可靠。例如,编译器可以进行代数重写和优化,因为它可以安全地用表达式的值替代表达式。

引用透明性的例子

  • 纯函数:
function add(x, y) {
  return x + y;
}

const result1 = add(2, 3);
const result2 = 2 + 3; // 5

// 引用透明性示例
console.log(result1 === result2); // true
  • 常量:
const pi = 3.14159;
const radius = 5;
const area = pi * radius * radius; // 3.14159 * 5 * 5 = 78.53975
  • 变量与副作用:
let x = 10;

function multiplyByTwo() {
  x *= 2;
  return x;
}

// 调用函数
const result1 = multiplyByTwo(); // 20
const result2 = multiplyByTwo(); // 40

// 结果不一致,因函数有副作用,引用不透明,结果不可替代

函数作为一等公民

函数作为一等公民(First-Class Citizen)一等对象(First-Class Object) 指的是函数在语言中被当作一种基本的数据类型对待

可以作为变量赋值

函数可以被赋值给变量,从而可以通过变量调用该函数。

// 函数表达式:函数赋值给了greet
const greet = function(name) {
  return `Hello, ${name}!`;
};

console.log(greet('Alice')); // Hello, Alice!

可以作为参数传递

函数可以作为参数传递给其他函数,允许更高层次的抽象和操作。

function processUserInput(callback) {
  const name = prompt('Please enter your name.');
  callback(name);
}

function greet(name) {
  console.log(`Hello, ${name}!`);
}

// greet函数作为参数传递给了函数processUserInput:
processUserInput(greet);

可以作为返回值

函数可以作为另一个函数的返回值,这允许函数生成其他函数。

function multiplier(factor) {
  // 返回一个新的函数
  return function(x) {
    return x * factor;
  };
}

const double = multiplier(2);
console.log(double(5)); // 10

可以被存储在数据结构中

函数可以被存储在数组、对象等数据结构中,并可以动态调用。

// 定义一组操作函数
const operations = [
  function(x) { return x + 1; }, // 增加 1
  function(x) { return x * 2; }, // 乘以 2
  function(x) { return x - 3; }  // 减去 3
];

// 定义一个函数来应用所有操作
function applyOperations(value, ops) {
  return ops.reduce((acc, operation) => operation(acc), value);
}

// 测试应用所有操作
const initialValue = 5;
const result = applyOperations(initialValue, operations);
// 输出过程
// 5 -> 5 + 1 = 6
// 6 -> 6 * 2 = 12
// 12 -> 12 - 3 = 9

console.log(result); // 输出结果
// 9

可以被动态创建和操作

可以在运行时创建函数,并动态生成或修改函数。

const createFunction = (operator) => {
  if (operator === 'add') {
    return (a, b) => a + b;
  } else if (operator === 'subtract') {
    return (a, b) => a - b;
  }
};

const addFunction = createFunction('add');
console.log(addFunction(3, 4)); // 7

高阶函数

高阶函数(Higher-Order Function) 是指接收一个或多个函数作为参数,或返回一个函数的函数

函数作为参数

map 函数是一个常见的高阶函数,它接收一个函数和一个数组作为参数,对数组中的每个元素应用这个函数,并返回一个新数组。

const numbers = [1, 2, 3, 4, 5];

// 使用 map 函数将每个元素平方
const squares = numbers.map(x => x * x);

console.log(squares); // [1, 4, 9, 16, 25]

函数作为返回值

// createMultiplier 是一个高阶函数,它接收一个因子 factor,
// 并返回一个新的函数,这个函数将其输入值乘以该因子。
function createMultiplier(factor) {
  return function(x) {
    return x * factor;
  };
}

// 创建一个乘以 2 的函数
const double = createMultiplier(2);

console.log(double(5)); // 10

函数组合

// compose 是一个高阶函数,用于将多个函数组合成一个新的函数,这个新函数依次应用这些函数。
// 组合两个函数 f 和 g
function compose(f, g) {
  return function(x) {
    return f(g(x));
  };
}

function double(x) {
  return x * 2;
}

function square(x) {
  return x * x;
}

const doubleThenSquare = compose(square, double);

console.log(doubleThenSquare(3)); // 36

实现一些常见的高阶函数

forEach
const forEach = (array, fn) => {
    for (let arr of array) {
        fn(arr)
    }
}

const arr = [1, 3, 4, 7, 8]
forEach(arr, function(item) {
    console.log(item)
})
// 1 3 4 7 8
map
const map = (array, fn) => {
    let results = []
    for (let arr of array) {
        results.push(fn(arr))
    }
    return results
}

const arr = [1, 3, 4, 7, 8]
console.log(map(arr, (item) => item * item))
// [ 1, 9, 16, 49, 64 ]
filter
const filter = (array, fn) => {
    let results = []
    for (let arr of array) {
        fn(arr) && results.push(arr)
    }
    return results
}

const arr = [1, 3, 4, 7, 8]
console.log(filter(arr, (item) => item > 3))
// [ 4, 7, 8 ]
every
const every = (array, fn) => {
    let result = true
    for (let arr of array) {
        result = fn(arr)
        if (!result) {
            break
        }
    }
    return result
}

const arr = [1, 3, 4, 7, 8]
console.log(every(arr, (item) => item > 3))
// false
some
const some = (array, fn) => {
    let result = true
    for (let arr of array) {
        result = fn(arr)
        if (result) {
            break
        }
    }
    return result
}

const arr = [1, 3, 4, 7, 8]
console.log(some(arr, (item) => item > 3))
// true

函数式编程中的重要概念与技术

函数组合

函数组合(Function Composition) 是指将两个或多个函数组合成一个新的函数,使得新的函数依次应用这些函数。简单来说,函数组合就是“把函数拼接在一起”。

const compose = (f, g) => x => f(g(x));
const double = x => x * 2;
const increment = x => x + 1;
const doubleThenIncrement = compose(increment, double);
console.log(doubleThenIncrement(5)); // 11

手动实现

function compose(...functions) {
  return function (x) {
    return functions.reduceRight((acc, fn) => fn(acc), x);
  };
}

// 示例函数
const double = x => x * 2;
const square = x => x * x;

// 使用 compose 组合函数
const doubleThenSquare = compose(square, double);

console.log(doubleThenSquare(3)); // 36

插播一条:reduce的实现

Array.prototype.reduce = function(array, fn, initialValue) {
    let accumlator;
    if(initialValue != undefined) {
        accumlator = initialValue
    } else {
        accumlator = array[0]
    }

    if(initialValue === undefined) {
        for(let i = 1; i < array.length; i++) {
            accumlator = fn(accumlator, array[i])
        }
    } else {
        for(let arr of array) {
            accumlator = fn(accumlator, arr)
        }
    }
    return [accumlator]
}

使用 Ramda 实现

const R = require('ramda');

// 示例函数
const double = x => x * 2;
const square = x => x * x;

// 使用 Ramda 的 compose 组合函数
const doubleThenSquare = R.compose(square, double);

console.log(doubleThenSquare(3)); // 36

管道

管道操作符 |> (提案中的特性) 用于将一个表达式的输出传递给下一个表达式作为输入。

管道是一种数据处理模式,其中数据从一个处理阶段流入下一个阶段,每个阶段都对数据进行某种处理。最终,数据经过所有阶段的处理,得到最终结果。

手动实现

// 定义管道函数
function pipe(...functions) {
  return function (x) {
    return functions.reduce((acc, fn) => fn(acc), x);
  };
}

// 示例函数
const double = x => x * 2;
const square = x => x * x;
const increment = x => x + 1;

// 使用 pipe 组合函数
// 从左往右执行
const processNumber = pipe(double, square, increment);

console.log(processNumber(3)); // 37

Lodash的_.flow_.flowRight

const _ = require('lodash');

// 示例函数
const double = x => x * 2;
const square = x => x * x;
const increment = x => x + 1;

// 使用 _.flow 组合函数
// 从左往右执行
const processNumber = _.flow([
  double,
  square,
  increment
]);

console.log(processNumber(3)); // 37
const _ = require('lodash');

// 示例函数
const double = x => x * 2;
const square = x => x * x;
const increment = x => x + 1;

// 使用 _.flowRight 组合函数
// 从右往左执行
const processNumber = _.flowRight([
  increment,
  square,
  double
]);

console.log(processNumber(3)); // 37

柯里化

柯里化是将一个接受多个参数的函数转化为接受一个参数的函数,并返回接受其余参数的新函数的过程

简易的柯里化:

const curry = (fn) => (...args) => 
  args.length >= fn.length 
    ? fn(...args) 
    : curry(fn.bind(null, ...args));

const add = (a, b) => a + b;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)); // 3

详细请见:JavaScript中的函数柯里化(Currying)一文搞懂JavaScript中的函数柯里化

偏应用

偏应用(Partial Application) 是指创建一个新函数,该函数从原函数中继承部分参数。通过偏应用,可以将原函数的某些参数固定下来,生成一个新的函数,这个新函数只需提供剩余的参数即可调用

与柯里化的区别:柯里化是逐个应用参数,而偏应用是一次应用部分参数。

偏应用的实现

手动实现
// 手动实现偏应用函数
function partial(fn, ...partialArgs) {
  return function (...args) {
    return fn(...partialArgs, ...args);
  };
}

// 示例函数
function multiply(a, b, c) {
  return a * b * c;
}

// 使用偏应用固定前两个参数
const multiplyBy2And3 = partial(multiply, 2, 3);

console.log(multiplyBy2And3(4)); // 24
使用 bind 实现偏应用
// 示例函数
function greet(greeting, name) {
  return `${greeting}, ${name}!`;
}

// 使用 bind 绑定第一个参数
const greetHello = greet.bind(null, 'Hello');

console.log(greetHello('World')); // "Hello, World!"

偏应用的应用

简化函数调用

通过偏应用,可以简化函数调用,避免重复传递相同的参数。

// 示例函数
function sendEmail(to, subject, body) {
  console.log(`Sending email to ${to} with subject ${subject}`);
  // 发送邮件的逻辑...
}

// 使用偏应用固定邮件主题
const sendWelcomeEmail = partial(sendEmail, 'Welcome!');

sendWelcomeEmail('user@example.com'); 
// Sending email to user@example.com with subject Welcome!
函数组合

偏应用可以与函数组合结合使用,提高代码的灵活性和可读性。

// 示例函数
const add = x => y => x + y;
const multiply = x => y => x * y;

// 使用偏应用和函数组合
const add10 = add(10);
const multiplyBy3 = multiply(3);

const calculate = pipe(add10, multiplyBy3);

console.log(calculate(5)); // (5 + 10) * 3 = 45

闭包

闭包(Closure) 是指在 JavaScript 中,一个函数能够记住并访问它定义时的作用域(即其词法环境)的机制,即使这个函数在其定义的作用域之外被调用。简单来说,闭包允许函数访问其创建时的外部作用域中的变量

  • 在 JavaScript 中,函数在定义时会形成一个作用域链。闭包利用这个作用域链来访问函数外部的变量。
  • 闭包是一个函数加上其环境。环境包含函数定义时可用的所有变量。
  • 闭包可以保存其创建时的状态,这使得函数能够持久地访问和操作外部变量。

闭包的本质: 函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员


符合以下两点即是闭包:

  1. 在该函数外部,对该函数内部有引用
  2. 在另一个作用域访问到 该函数作用域中的局部成员

闭包有3个可访问的作用域:

  • 在它自身声明之内声明的变量
  • 对全局变量的访问
  • 对外部函数变量的访问

闭包的例子

只执行一次(Once)
function once(fn) {
    let done = false
    return function() {
        if (!done) {
            done = true
            return fn.apply(fn, arguments)
        } else {
          console.log(`函数${fn.name}只能调用一次!`)
        }
    }
}

function pay(money) {
    console.log(`支付${money}元`)
}

const payOnce = once(pay)
payOnce(5)
// 支付5元
payOnce(5)
// 函数pay只能调用一次!
私有数据管理
function createPerson(name) {
  // age 是私有的,只能通过 setAge 和 getAge 方法访问和修改
  let age = 0;
  
  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return age;
    },
    setAge: function(newAge) {
      if (newAge > age) {
        age = newAge;
      }
    }
  };
}

const person = createPerson('John');
console.log(person.getName()); // John
console.log(person.getAge());  // 0
person.setAge(25);
console.log(person.getAge());  // 25
函数工厂
function multiplyBy(factor) {
  return function(x) {
    return x * factor;
  };
}

const double = multiplyBy(2);
const triple = multiplyBy(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15
模块模式
// CounterModule 是一个立即执行函数表达式(IIFE),它返回一个带有 increment、decrement 和 getCount 方法的对象。
// count 变量是私有的,只能通过这些方法访问和修改。
const CounterModule = (function() {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
})();

console.log(CounterModule.increment()); // 1
console.log(CounterModule.increment()); // 2
console.log(CounterModule.decrement()); // 1
console.log(CounterModule.getCount());  // 1
延迟执行
function delayMessage(message, delay) {
  return function() {
    setTimeout(() => {
      console.log(message);
    }, delay);
  };
}

const delayedHello = delayMessage('Hello after 2 seconds', 2000);
delayedHello(); // "Hello after 2 seconds" (after 2 seconds)
记忆化
function memoize(fn) {
  const cache = {};

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    const result = fn(...args);
    cache[key] = result;
    return result;
  };
}

const factorial = memoize(function(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
});

console.log(factorial(5)); // 120
console.log(factorial(6)); // 720 (使用缓存计算)
自定义计时器
function createTimer() {
  let startTime = 0;
  let elapsedTime = 0;
  let timerId;

  function start() {
    startTime = Date.now();
    timerId = setInterval(() => {
      elapsedTime = Date.now() - startTime;
      console.log(`Elapsed time: ${elapsedTime}ms`);
    }, 1000);
  }

  function stop() {
    clearInterval(timerId);
  }

  function reset() {
    elapsedTime = 0;
    startTime = Date.now();
  }

  return { start, stop, reset };
}

const timer = createTimer();
timer.start(); // 开始计时器,1秒输出一次时间
setTimeout(timer.stop, 5000); // 5秒后停止计时器
生成唯一ID
function createIdGenerator() {
  let id = 0;

  return function() {
    id += 1;
    return id;
  };
}

const generateId = createIdGenerator();

console.log(generateId()); // 1
console.log(generateId()); // 2
console.log(generateId()); // 3
事件订阅/发布模式
function createPubSub() {
  const subscribers = {};

  return {
    subscribe(event, callback) {
      if (!subscribers[event]) {
        subscribers[event] = [];
      }
      subscribers[event].push(callback);
    },
    publish(event, data) {
      if (subscribers[event]) {
        subscribers[event].forEach(callback => callback(data));
      }
    }
  };
}

const pubsub = createPubSub();

pubsub.subscribe('message', (data) => {
  console.log(`Received message: ${data}`);
});

pubsub.publish('message', 'Hello, World!'); // "Received message: Hello, World!"
状态机
function createStateMachine(initialState) {
  let state = initialState;

  function transition(newState) {
    state = newState;
    console.log(`State changed to: ${state}`);
  }

  function getState() {
    return state;
  }

  return { transition, getState };
}

const fsm = createStateMachine('idle');
console.log(fsm.getState()); // "idle"
fsm.transition('running'); // "State changed to: running"
console.log(fsm.getState()); // "running"
fsm.transition('stopped'); // "State changed to: stopped"
动态模板渲染
function createTemplateRenderer(template) {
  return function(data) {
    return template.replace(/{{(\w+)}}/g, (match, key) => data[key] || '');
  };
}

const template = 'Hello, {{name}}! Welcome to {{location}}.';
const render = createTemplateRenderer(template);

console.log(render({ name: 'John', location: 'New York' })); // "Hello, John! Welcome to New York."
console.log(render({ name: 'Jane', location: 'San Francisco' })); // "Hello, Jane! Welcome to San Francisco."
自定义事件系统
function createEventEmitter() {
  const events = {};

  function on(event, listener) {
    if (!events[event]) {
      events[event] = [];
    }
    events[event].push(listener);
  }

  function off(event, listener) {
    if (events[event]) {
      events[event] = events[event].filter(l => l !== listener);
    }
  }

  function emit(event, data) {
    if (events[event]) {
      events[event].forEach(listener => listener(data));
    }
  }

  return { on, off, emit };
}

const emitter = createEventEmitter();
function responseHandler(data) {
  console.log(`Response received: ${data}`);
}

emitter.on('response', responseHandler);
emitter.emit('response', 'Success'); // "Response received: Success"
emitter.off('response', responseHandler);
emitter.emit('response', 'Success'); // (No output)

递归

递归是编程中一种常见的技术,它指的是函数调用自身来解决问题。递归通常用于分解复杂的问题,将其拆解为更小、更易解决的子问题,然后将这些子问题的结果组合起来得到最终结果。

递归分为直接递归间接递归

  • 直接递归:函数直接调用自身。
  • 间接递归:函数通过其他函数间接调用自身。

递归函数通常包含两个主要部分:

  1. 基线条件(Base Case) :防止无限递归,是递归停止的条件。
  2. 递归条件(Recursive Case) :函数继续调用自身,缩小问题的规模。

尾调用

尾调用(Tail Call)是指在一个函数的最后一步调用另一个函数(包括调用自身),并且函数的 返回结果直接是这个调用的返回值。在尾调用的情况下,当前函数的执行完成之后不需要做其他操作,返回值就是尾调用函数的返回值。

function f(x) {
  return g(x); // g(x) 是 f(x) 的尾调用
}

function g(x) {
  return x + 1;
}

console.log(f(5)); // 6

注意:尾调用最后只能是返回函数调用,而不能是别的。 以下f1f2f3都不是尾调用:

function g(x) {
    return x
} 

function f1(x) {
    return g(x) + 1
    // return的不是函数调用,还进行了其他计算
}

function f2(x) {
    const y = g(x)
    return y
    // 没有return函数调用
}

function f3(x) {
    g(x)
    // 实际上return的是undefined
}

尾递归优化

尾递归优化(Tail Recursion Optimization,简称 TRO)是编译器或解释器对尾递归函数进行的一种优化处理。它可以在一定条件下减少递归函数调用的开销,从而提高程序的运行效率,并防止因递归深度过大而导致的栈溢出(Stack Overflow)问题

注意:

  • 并非所有递归都能转换为尾递归:只有在函数返回结果完全依赖于递归调用结果时,才能转换为尾递归。
  • 尾递归优化的可移植性:不同语言和环境对尾递归优化的支持程度不同。在不支持尾递归优化的环境中,过深的递归仍然可能导致栈溢出。
  • 使用累加器:为了将普通递归转换为尾递归,通常需要引入一个累加器参数,用于在每次递归时携带中间结果。
尾递归

尾递归(Tail Recursion)是递归的一种特殊形式,也是尾调用的一种特殊形式,指的是在一个函数的最后一步是直接返回递归调用的结果,并且不再进行其他操作。换句话说,在尾递归中,递归调用的返回值直接就是函数的返回值

尾递归的一般形式:

function tailRecursiveFunction(params) {
  if (baseCondition) {
    return baseResult;
  } else {
    return tailRecursiveFunction(newParams);
  }
}

尾递归的优势:

  • 节省栈空间在尾递归中,当前函数的栈帧在递归调用之后不再需要,因此编译器或解释器可以选择直接复用这个栈帧,从而不会随着递归深度增加而增加栈帧的数量
  • 防止栈溢出:由于尾递归优化后不会增加栈帧的深度,因此即使递归调用次数非常多,也不会导致栈溢出。

尾递归优化的原理:

  • 在常规递归中,每次函数调用都会创建一个新的栈帧来保存当前函数的状态(包括参数、局部变量和返回地址等)。这些栈帧会在函数调用结束时逐一弹出。
  • 而在尾递归中,由于递归调用是函数的最后一步,当前栈帧的所有状态在递归调用后都不再需要,因此可以直接复用当前栈帧,而无需创建新的栈帧整个过程中只有一个栈桢
尾递归与普通递归比较
阶乘计算
  1. 普通递归计算阶乘:
function factorial(n) {
  if (n === 0) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

console.log(factorial(5)); // 输出 120
  • 逐层递归展开:普通递归的计算是从 n 逐步递归到 0,每一层都需要等待下一层的结果才能继续计算。
  • 栈空间使用多:每次递归调用都会创建一个新的栈帧,因此递归的深度等同于栈帧的数量。当 n 较大时,递归深度增加,可能会导致栈溢出。
  • 时间复杂度:时间复杂度为 O(n)O(n),即阶乘的计算时间与 n 成线性关系。
  1. 尾递归计算阶乘:
function factorialTailRecursive(n, accumulator = 1) {
  if (n === 0 || n === 1) {
    return accumulator;
  } else {
    return factorialTailRecursive(n - 1, n * accumulator);
  }
}

console.log(factorialTailRecursive(5)); // 120
// 在 factorialTailRecursive 函数中,accumulator 参数用于累积乘积结果。
// 每次递归调用都会将当前的乘积结果传递给下一个递归调用。
// 递归调用是函数的最后一步,没有其他操作跟在递归调用之后,因此这是尾递归。
  • 累积器优化:尾递归版本通过引入 accumulator 累积器参数,将每一步的计算结果传递到下一次递归调用中,避免了普通递归的层层展开。
  • 栈空间使用少:如果编译器或解释器支持尾递归优化(TCO),尾递归可以复用栈帧,不会随着递归深度增加而增加栈的使用,从而避免栈溢出。
  • 时间复杂度:与普通递归一样,尾递归的时间复杂度也是 O(n)O(n),但由于尾递归优化,实际执行效率更高,尤其在支持 TCO 的环境中。
  1. 循环迭代实现:
function factorialIterative(n) {
  let result = 1;
  for (let i = 1; i <= n; i++) {
    result *= i;
  }
  return result;
}

console.log(factorialIterative(5)); // 输出 120
斐波那契数列
  1. 普通递归:
function fibonacci(n) {
  if (n === 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

console.log(fibonacci(10)); // 输出 55
  • 重复计算:每次计算 F(n)F(n) 时,都需要重新计算 F(n1)F(n - 1)F(n2)F(n - 2),导致大量重复计算。例如,计算 F(10)F(10) 时,fibonacci(9)fibonacci(8) 会被多次计算。
  • 时间复杂度高:由于存在大量的重复计算,普通递归的时间复杂度是指数级别的 O(2n)O(2^n),当 n 很大时,效率会非常低。
  • 栈空间使用大:每次递归调用都会创建新的栈帧,当 n 较大时,递归深度增加,可能会导致栈溢出。
  1. 尾递归实现:
function fibonacciTailRecursive(n, a = 0, b = 1) {
  if (n === 0) {
    return a;
  } else if (n === 1) {
    return b;
  } else {
    return fibonacciTailRecursive(n - 1, b, a + b);
  }
}

console.log(fibonacciTailRecursive(10)); // 输出 55
  • 无重复计算:尾递归利用额外的参数 ab 来保存计算的中间结果,避免了重复计算。
  • 时间复杂度低:尾递归的时间复杂度是线性级别的 O(n)O(n),每一步计算都仅依赖于前一步的结果,因此效率较高。
  • 栈空间使用少:如果编译器或解释器支持尾递归优化(TCO),尾递归可以复用栈帧,不会随着递归深度增加而增加栈的使用,从而避免栈溢出。
  1. 循环迭代实现:
function fibonacciIterative(n) {
  let a = 0, b = 1;
  for (let i = 0; i < n; i++) {
    [a, b] = [b, a + b];
  }
  return a;
}

console.log(fibonacciIterative(10)); // 输出 55
  1. 动态规划实现: 动态规划使用自底向上的方法,通过一个数组来存储计算结果,避免了递归调用和重复计算
function fibonacciDP(n) {
  if (n === 0) return 0;
  if (n === 1) return 1;

  let fib = [0, 1];
  for (let i = 2; i <= n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2];
  }
  return fib[n];
}

console.log(fibonacciDP(10)); // 输出 55

尾递归优化的支持

ECMAScript 2015(即 ES6)标准首次引入了对尾调用优化的要求。然而,实际情况是,许多 JavaScript 引擎并没有完全实现这一特性。

  • V8 引擎(Chrome、Node.js) :V8 引擎在早期版本中实现了一些尾调用优化,但后来为了兼容性等问题,取消了这一优化特性。因此,现代的 Chrome 浏览器和 Node.js 中通常不支持尾递归优化。
  • SpiderMonkey 引擎(Firefox) :同样地,Firefox 的 JavaScript 引擎在早期也尝试支持尾递归优化,但由于兼容性和性能的考虑,这一特性未能被广泛采用。
  • JavaScriptCore 引擎(Safari) :Safari 的引擎 JavaScriptCore 在某些版本中支持尾递归优化,但整体支持不稳定。

支持尾递归优化的语言或环境,通常会在检测到尾递归时执行如下优化:

  • 复用栈帧:在尾递归调用时,复用当前栈帧,避免栈帧的增长。
  • 跳转而非调用:在尾递归调用时,进行跳转而不是函数调用,从而降低函数调用的开销。

惰性函数

惰性函数(Lazy Function)是一种设计模式,旨在延迟计算,直到函数的结果确实需要使用时才执行计算。惰性函数的一个常见应用场景是优化程序性能,尤其是在某些计算代价较高或结果不总是需要的情况下。惰性函数可以通过在第一次调用时计算结果,并将该结果缓存起来,以避免后续的重复计算。

  • 第一次调用:执行必要的计算,并将结果存储或缓存起来。
  • 后续调用:直接返回已经计算并存储的结果,而不再重复计算。

惰性函数的实现

普通实现
// compute() 函数在第一次调用时执行,并缓存结果 result。
// 后续调用直接返回缓存的 result,避免了重复的计算。
function lazyFunction() {
  let result;

  // 实际计算的函数,只执行一次
  function compute() {
    console.log('Computing...');
    return 42; // 假设这个计算过程比较复杂
  }

  // 外部函数
  return function() {
    if (result === undefined) {
      result = compute(); // 第一次计算并缓存结果
    }
    return result; // 后续调用直接返回缓存结果
  };
}

const getLazyValue = lazyFunction();

console.log(getLazyValue()); // 输出 'Computing...' 和 42
console.log(getLazyValue()); // 仅输出 42,不再执行计算
闭包实现
// lazyValue 接受一个函数 fn,该函数包含要延迟的计算。
// 第一次调用 getLazyResult() 时,会执行 expensiveCalculation(),并缓存结果 value。
// 后续调用直接返回 value,而不再执行计算。
function lazyValue(fn) {
  let computed = false;
  let value;

  return function() {
    if (!computed) {
      value = fn();
      computed = true;
    }
    return value;
  };
}

const expensiveCalculation = () => {
  console.log('Performing expensive calculation...');
  return 99 * 99;
};

const getLazyResult = lazyValue(expensiveCalculation);

console.log(getLazyResult()); // 输出 'Performing expensive calculation...' 和 9801
console.log(getLazyResult()); // 仅输出 9801,不再执行计算

惰性函数的应用

惰性函数初始化模式

函数在第一次调用时初始化其行为,并通过重写函数来保持后续调用的一致性。

function lazyInitialization() {
  console.log('Initial computation...');

  // 重写函数,之后的调用将不会再执行初始化逻辑
  lazyInitialization = function() {
    console.log('Already initialized.');
  };

  // 初次执行时的行为
  return 'Initialized Value';
}

console.log(lazyInitialization()); // 输出 'Initial computation...' 和 'Initialized Value'
console.log(lazyInitialization()); // 仅输出 'Already initialized.'
惰性函数与模块模式

惰性函数与模块模式结合,延迟模块的初始化或加载

const LazyModule = (function() {
  let initialized = false;

  function initialize() {
    console.log('Initializing module...');
    // 模块的初始化逻辑
    initialized = true;
  }

  return {
    doSomething: function() {
      if (!initialized) {
        initialize();
      }
      console.log('Doing something...');
    }
  };
})();

LazyModule.doSomething(); // 输出 'Initializing module...' 和 'Doing something...'
LazyModule.doSomething(); // 仅输出 'Doing something...'

函子

  • 在数学上,函子是一种映射,它将一个范畴中的对象和态射(morphism)映射到另一个范畴中,同时保持范畴结构不变。
  • 在编程中,函子是一种封装了值和操作的类型,它允许我们对值进行变换,而不解包(unwrap)这个值。
  • 函子可以用来管理值和值的变化过程,把异常和异步操作等副作用控制在可控的范围之内。
  • 函子是为了解决纯函数中的异常带来的副作用,函子可以帮我们控制副作用(IO),进行异常处理(Either)或异步任务(Task)

Container

如果一个对象内部持有一个值,包含值和值的变形关系(这个变形关系就是函数) ,则称之为容器Container。

class Container {
    constructor(value) {
        this.value = value
    }
}
const container = new Container(1)
console.log(container.value) // 1

Functor

函子可以简单地理解为一个实现了 map 函数(映射)的容器。它接受一个容器(通常是一个对象或者结构),并提供了一种将函数作用于容器内数据的方式,而不改变容器的结构。

在编程中,函子通常表现为一个对象,它包含了一个值,并且提供了一个 map 方法,map 方法将一个函数应用于这个值并返回一个新的函子,也就是将函子映射成一个新的函子。其形式如下:

class Functor {
  constructor(value) {
    this.value = value
  }

  map(fn) {
    return new Functor(fn(this.value))
  }
}
console.log(new Functor(5).map((x) => x + 1).map((x) => x * x))
// Functor { value: 36 }

函数式编程的运算不直接操作值,而是由函子完成 函子就是个实现了 map 方法的对象 我们可以把函子想象成一个盒子,这个盒子里封装了一个值,想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数) ,由这个函数来对值进行处理 最终map方法返回一个包含新值的盒子(函子)


数组(Array)是一个天然的函子,因为数组本身就实现了map方法

const arr = [1, 2, 3];
const newArr = arr.map(x => x * 2);
console.log(newArr); // 输出 [2, 4, 6]

函子的特性

  • 保持结构不变: 当我们对函子的值应用 map 时,函子的结构不会改变。map 方法只会变换函子内的值,并返回一个同类型的新函子。
const functor = new Functor(10);
const newFunctor = functor.map(x => x * 2);
console.log(newFunctor instanceof Functor); // 输出 true
  • 保持可组合性(结合律) : 函子必须保持可组合性,这意味着我们可以将多个 map 操作链式组合起来,而不改变结果。
const double = x => x * 2;
const increment = x => x + 1;

const result1 = new Functor(2).map(double).map(increment);
const result2 = new Functor(2).map(x => increment(double(x)));

console.log(result1.value); // 输出 5
console.log(result2.value); // 输出 5

Pointed函子

Pointed 函子是实现了of 静态方法的函子 of方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文Context (把值放到容器中,使用map来处理值)。

class PointedContainer {
    static of(value) {
        return new PointedContainer(value)
    }
    constructor(value) {
        this._value = value
    }

    map(fn) {
        return PointedContainer.of(fn(this._value))
    }
}
const pointedContainer = PointedContainer.of(1)
console.log(pointedContainer)
// PointedContainer { _value: 1 }
console.log(pointedContainer.map(value => value + 1)) 
// PointedContainer { _value: 2 }

MayBe函子

Maybe 用于处理可能为 nullundefined 的值。它通常有两个状态:

  • Just(value) 表示存在的值。
  • Nothing 表示不存在的值。

Maybe 函子使我们能够安全地处理可能不存在的值,而无需显式检查 nullundefined

Maybe 函子的 map 方法只在 Just 的情况下应用函数,而在 Nothing 的情况下返回 Nothing

class Maybe {
    constructor(value) {
        this.value = value;
    }

    static just(value) {
        return new Maybe(value);
    }

    static nothing() {
        return new Maybe(null);
    }

    map(fn) {
        return this.value !== null ? Maybe.just(fn(this.value)) : Maybe.nothing();
    }
}

// 使用例子
const maybeValue = Maybe.just(5).map(x => x * 2);
console.log(maybeValue.value); // 输出 10

const maybeNothing = Maybe.nothing().map(x => x * 2);
console.log(maybeNothing.value); // 输出 null

Either函子

Either 函子通常用于表示一种可能有两种状态的值:

  • Right(value):表示成功状态,值存在。
  • Left(value):表示失败状态,包含错误信息。

Either 函子用于处理可能成功或失败的计算,通常用于错误处理;也可以用来处理默认值。

map 方法通常只在 Right 状态下应用函数,而在 Left 状态下保持不变。

右边有值就用右边的,没有就用左边的

class Either {
  constructor(left, right) {
    this.left = left
    this.right = right
  }

  static right(value) {
    return new Either(null, value)
  }

  static left(value) {
    return new Either(value, null)
  }

  map(fn) {
    return this.right !== null
      ? Either.right(fn(this.right))
      : Either.left(this.left)
  }

  get value() {
    return this.right || this.left
  }
}
  
// 使用例子
const eitherSuccess = Either.right(10).map(x => x * 2);
console.log(eitherSuccess.left); // 输出 null
console.log(eitherSuccess.right); // 输出 20
console.log(eitherSuccess.value); // 输出 20

const eitherError = Either.left('Error occurred').map(x => x * 2);
console.log(eitherError.left); // 输出 'Error occurred'
console.log(eitherError.right); // 输出 null
console.log(eitherError.value); // 输出 'Error occurred'

AP函子

AP函子(Applicative Functor,简称为AP), 是一种实现了 ap 方法的函子, 允许我们将包含在容器中的函数应用于另一个容器中的值。

ap方法可以让一个函子内的函数使用另一个函子的值进行计算。 ap方法的参数不是函数,而是另一个函子

class Ap {
    static of(value) {
      return new Ap(value)
    }

    constructor(value) {
      this.value = value
    }

    ap(functor) {
        return Ap.of(this.value(functor.value))
    }
}
// A的value是一个函数
const A = new Ap(x => x + 1)
// B的value是一个值
const B = new Ap(2)
const result = A.ap(B)
console.log(result.value) // 3

Monad函子——单子

函子的值也可以是函子,这样会出现多层函子嵌套的情况。

Monad(单子【不可分割的实体】)函子的作用是,总是返回一个单层的函子

单子(Monad)不仅是一个函子(Functor),也是一种设计模式,用于处理计算中的副作用(如状态、I/O、异常处理等),并以一种结构化的方式将一系列操作组合起来。

Monad 可以看作是Applicative Functor(Ap函子)的进一步扩展,它不仅允许我们对容器内的值进行操作,还允许我们将多个操作组合在一起。

单子在保证代码纯净性的同时,允许处理那些通常在纯函数式编程中难以处理的任务。

Monad有两个核心方法:

  • of (或unit / return : 该方法将一个值放入容器中,创建一个Monad实例。
  • flatMap (或bind / chain : 该方法允许我们将一个返回Monad的函数应用于一个已有的Monad,避免嵌套的容器。

单子有一个flatMp方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值 ,保证返回的永远是一个单层的容器,不会出现嵌套的情况。

class Monad {
    constructor(value) {
        this.value = value
    }

    static of(value) {
        return new Monad(value)
    }

    map(fn) {
        return Monad.of(fn(this.value))
    }

    join() {
        return this.value
    }

    flatMap(fn) {
        return this.map(fn).join()
    }
}

const result = Monad.of('a')
                .flatMap(str => Monad.of(str.toUpperCase()))
                .flatMap(str => Monad.of(str + 'b'))
                .flatMap(str => Monad.of(str + 'c'))
console.log(result) 
// Monad { value: 'Abc' }
console.log(result.join())
// Abc

IO函子

IO函子 是函数式编程中的一种特殊函子,用于处理带有副作用的操作(如读写文件、打印日志、获取当前时间、处理用户输入等)

在纯函数式编程中,函数应该是纯净的,即相同的输入总是产生相同的输出,且没有副作用。而现实中,程序通常需要与外部世界交互(这往往带来副作用)。IO函子通过 将副作用延迟到真正执行时才发生 ,帮助我们保持函数的纯净性

通过使用IO函子,我们不会立即执行副作用操作,而是将操作封装在函子内部,延迟到需要时再执行。

IO函子特点

IO函子有以下几个重要的特点:

  • 延迟执行:IO函子内部封装的副作用操作不会立即执行,而是被延迟到明确调用 run 方法时才执行。
  • 保持纯函数:通过将副作用操作封装在IO函子中,我们可以保持大多数代码的纯净性,使得函数式编程的优势(如易测试性、可组合性)得以保留。
  • 可组合性:使用 mapflatMap,我们可以将多个IO操作组合成复杂的行为,并通过链式调用保持代码的清晰和简洁。
class IO {
  constructor(effect) {
    if (typeof effect !== 'function') {
      throw new Error('IO Usage: function required')
    }
    this.effect = effect
  }

  // of 方法用于将值放入 IO 容器中
  static of(value) {
    return new IO(() => value)
  }

  // map 方法将一个函数应用于 IO 内部的值
  map(fn) {
    return new IO(() => fn(this.effect()))
  }

  // flatMap 方法将一个返回 IO 的函数应用于 IO 内部的值
  flatMap(fn) {
    return new IO(() => fn(this.effect()).run())
  }

  // run 方法用于执行 IO 函子内部封装的操作
  run() {
    return this.effect()
  }
}

示例

  • 封装副作用操作:
// 创建一个 IO 函子,用于打印输出
const print = new IO(() => console.log('Hello, World!'));

// 运行 IO 函子,真正执行操作
print.run(); // 输出 'Hello, World!'
  • 组合多个IO操作
const read = new IO(() => 'hello, world');

// 这里我们定义 write 函数,它接收一个字符串并返回一个 IO 函子
const write = (text) => new IO(() => console.log(text));

// 使用 flatMap 将两个操作组合在一起
const readAndWrite = read.flatMap((text) => write(text));

// 执行组合操作
readAndWrite.run(); // 输出: hello, world

应用

  1. 处理IO操作
// test.txt:
// 这是一段测试内容!!!
// -------------------------------------------------------------------
const readFile = filename => new IO(() => {
  const fs = require('fs');
  return fs.readFileSync(filename, 'utf-8');
});

const printContent = content => new IO(() => {
  console.log(content);
});

// 组合文件读取和内容打印操作
const readAndPrint = filename =>
  readFile(filename).flatMap(printContent);

// 执行组合操作
readAndPrint('test.txt').run();
// 这是一段测试内容!!!
  1. 处理异步操作
class AsyncIO extends IO {
  // 重写 flatMap 来处理异步操作
  flatMap(fn) {
    return new AsyncIO(() => {
      return this.effect().then(result => fn(result).run());
    });
  }

  // run 方法用于执行异步操作
  run() {
    return this.effect();
  }
}

// 示例:异步获取数据
const fetchData = url =>
  new AsyncIO(() => fetch(url).then(response => response.json()));

const logData = data =>
  new AsyncIO(() => {
    console.log(data);
    return data; // 确保数据传递
  });

// 组合异步获取数据和打印操作
const fetchAndLog = url =>
  fetchData(url).flatMap(logData);

// 执行组合操作
fetchAndLog('https://jsonplaceholder.typicode.com/posts/1').run().then(() => {
  console.log('Data fetched and logged successfully');
});
// {
//   userId: 1,
//   id: 1,
//   title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
//   body: 'quia et suscipit\n' +
//     'suscipit recusandae consequuntur expedita et cum\n' +
//     'reprehenderit molestiae ut ut quas totam\n' +
//     'nostrum rerum est autem sunt rem eveniet architecto'
// }
// Data fetched and logged successfully

Task函子(异步执行)

与传统的 Promise 类似,Task 函子可以封装异步操作并支持链式操作,但它更符合函数式编程的思想,避免了副作用,推迟了执行,直到明确地要求执行

特点

  • 惰性求值Task 函子不会立即执行封装的异步操作。与 Promise 不同的是,Task 只在你明确地调用时(通常通过 fork 方法)才会执行。这种惰性求值使得 Task 更容易组合和处理。
  • 不变性:一旦创建,Task 的内容不会改变。所有的链式操作(如 mapflatMap)都会返回新的 Task 函子,而不会修改原来的 Task
  • 强大的组合性Task 可以与其他函子进行组合,也可以通过 mapflatMap 等方法对异步操作进行组合,形成复杂的异步处理流程。

实现

class Task {
  constructor(fork) {
    this.fork = fork
  }

  // map 方法将一个函数应用于 Task 的成功结果
  map(fn) {
    return new Task((reject, resolve) =>
      this.fork(reject, (result) => resolve(fn(result))),
    )
  }

  // flatMap 方法将一个返回 Task 的函数应用于 Task 的成功结果
  flatMap(fn) {
    return new Task((reject, resolve) =>
      this.fork(reject, (result) => fn(result).fork(reject, resolve)),
    )
  }

  // of 方法用于创建一个立即成功的 Task
  static of(value) {
    return new Task((_, resolve) => resolve(value))
  }

  // fork 方法用于执行 Task
  fork(reject, resolve) {
    return this.fork(reject, resolve)
  }
}

示例

// 创建一个异步任务,用于异步操作
const fetchTask = (url) =>
  new Task((reject, resolve) => {
    fetch(url)
      .then((response) => response.json())
      .then(resolve)
      .catch(reject)
  })

// 打印任务,用于打印结果
const logTask = (data) =>
  new Task((_, resolve) => {
    console.log(data)
    resolve(data)
  })

// 组合任务:获取数据并打印
const fetchAndLogTask = (url) => fetchTask(url).flatMap(logTask)

// 执行任务
fetchAndLogTask('https://jsonplaceholder.typicode.com/posts/1').fork(
  (err) => console.error(err),
  () => console.log('Task completed successfully'),
)
// {
//   userId: 1,
//   id: 1,
//   title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
//   body: 'quia et suscipit\n' +
//     'suscipit recusandae consequuntur expedita et cum\n' +
//     'reprehenderit molestiae ut ut quas totam\n' +
//     'nostrum rerum est autem sunt rem eveniet architecto'
// }
// Task completed successfully
  • fetchTask:封装了一个异步 fetch 请求,返回 Task 函子。该函子可以通过 fork 方法来处理请求的成功或失败。
  • logTask:创建一个任务来打印数据。注意,这里没有立即执行 console.log,而是通过 Task 的惰性求值,在合适的时机执行。
  • fetchAndLogTask:组合了 fetchTasklogTask,使用 flatMap 将数据从 fetchTask 传递到 logTask 中。
  • fork 方法:在调用 fetchAndLogTask(url) 后,通过 fork 方法明确地执行整个异步任务链。fork 接受两个回调函数:一个用于处理失败,一个用于处理成功。

Task vs. Promise

  • 惰性Task 不会立即执行,只有在调用 fork 时才执行。Promise 则在创建时就立即执行。
  • 可组合性Task 可以通过 flatMap 等方法组合多个异步操作,构建更复杂的流程,而 Promise 的组合性相对较弱。