JavaScript内核与高级编程之:`JavaScript`的`Compose`和`Pipe`:从右到左和从左到右的函数组合。

各位观众老爷,大家好!今天咱们来聊聊JavaScript里两个非常有趣,而且在函数式编程中举足轻重的家伙:composepipe。 这俩兄弟,一个从右往左,一个从左往右,专门负责把一堆函数像流水线一样串起来。 准备好了吗?咱们这就开始!

1. 函数组合:为什么要这么干?

先问大家一个问题:你们平时写代码,是不是经常会遇到这样的情况?

const number = 5;
const doubled = number * 2;
const squared = doubled * doubled;
const result = squared + 10;

console.log(result); // 110

这段代码很简单,对一个数字进行了翻倍、平方,最后加10。 但如果逻辑更复杂一些,或者需要多次复用这个流程,代码就会变得冗长而难以维护。

函数组合就是用来解决这个问题的。 它的核心思想是:把多个函数像搭积木一样组合起来,形成一个新的函数,这个新函数可以一次性完成所有操作。

用函数组合的方式,上面的代码可以这样写(先别管 compose 是什么,后面会详细解释):

const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;

const composedFunction = compose(addTen, square, double); // 注意顺序!

const result = composedFunction(5);

console.log(result); // 110

看到了吗? 我们把 doublesquareaddTen 这三个函数组合成了一个新的函数 composedFunction。 调用 composedFunction(5) 就能得到和之前一样的结果。

这样做的好处是什么呢?

  • 代码更简洁: 避免了中间变量,代码更易读。
  • 可复用性更高: 可以把常用的函数组合封装成一个独立的函数,方便在不同的地方使用。
  • 更易于测试: 每个小函数都是独立的,更容易进行单元测试。
  • 更贴近函数式编程的思想: 函数组合是函数式编程的核心概念之一。

2. compose:从右向左的函数组合

compose 函数的作用是将多个函数从右向左依次执行,并将前一个函数的返回值作为后一个函数的输入。

用人话说,就像叠罗汉,最下面的函数先执行,然后把结果给上面的函数,依次往上,直到最上面的函数执行完毕。

一个简单的 compose 函数的实现如下:

const compose = (...fns) => {
  return (arg) => {
    return fns.reduceRight((result, fn) => {
      return fn(result);
    }, arg);
  };
};

这段代码看着可能有点晕,咱们来一步步解释:

  • ...fns: 使用剩余参数语法,将所有传入的函数收集到一个数组 fns 中。
  • return (arg) => { ... }compose 函数返回一个函数,这个函数接收一个参数 arg,这个参数将作为最右边(也就是第一个执行)的函数的输入。
  • fns.reduceRight((result, fn) => { ... }, arg): 使用 reduceRight 方法从右向左遍历 fns 数组。
    • result: 累积值,初始值为 arg
    • fn: 当前遍历到的函数。
    • fn(result): 将 result 作为 fn 的输入,并返回 fn 的执行结果,作为下一次 reduceRightresult

简单来说,reduceRight 就是从数组的最后一个元素开始,依次将每个元素(这里是函数)应用到累积值上,最终返回一个累积值。

现在,让我们用 compose 函数来改造之前的例子:

const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;

const composedFunction = compose(addTen, square, double);

const result = composedFunction(5);

console.log(result); // 110

在这个例子中,compose(addTen, square, double) 的执行顺序是:

  1. double(5) 得到 10
  2. square(10) 得到 100
  3. addTen(100) 得到 110

最终 composedFunction(5) 的结果就是 110。

compose 的一些注意事项

  • 参数类型: compose 组合的函数需要满足一个条件:前一个函数的返回值类型必须是后一个函数可以接受的参数类型。 否则,就会出错。
  • 执行顺序: compose 的执行顺序是从右向左的,这对于理解函数组合至关重要。
  • 参数数量: compose 组合的函数可以是单参数函数,也可以是多参数函数,但只有最右边的函数可以接收多个参数。 比如,compose(f, g, h)h 可以接收多个参数,但 gf 只能接收一个参数。

3. pipe:从左向右的函数组合

pipe 函数和 compose 函数的功能类似,都是将多个函数组合成一个函数。 唯一的区别是,pipe 函数的执行顺序是从左向右的。

用人话说,pipe 就像一条管道,数据从管道的一端流入,经过一系列函数的处理,最终从管道的另一端流出。

一个简单的 pipe 函数的实现如下:

const pipe = (...fns) => {
  return (arg) => {
    return fns.reduce((result, fn) => {
      return fn(result);
    }, arg);
  };
};

这段代码和 compose 函数的代码几乎一样,唯一的区别是使用了 reduce 方法而不是 reduceRight 方法。

现在,让我们用 pipe 函数来改造之前的例子:

const double = x => x * 2;
const square = x => x * x;
const addTen = x => x + 10;

const pipedFunction = pipe(double, square, addTen);

const result = pipedFunction(5);

console.log(result); // 110

在这个例子中,pipe(double, square, addTen) 的执行顺序是:

  1. double(5) 得到 10
  2. square(10) 得到 100
  3. addTen(100) 得到 110

最终 pipedFunction(5) 的结果也是 110。

pipe 的一些注意事项

  • 参数类型:compose 一样,pipe 组合的函数也需要满足一个条件:前一个函数的返回值类型必须是后一个函数可以接受的参数类型。
  • 执行顺序: pipe 的执行顺序是从左向右的,这对于理解函数组合至关重要。
  • 参数数量: pipe 组合的函数可以是单参数函数,也可以是多参数函数,但只有最左边的函数可以接收多个参数。 比如,pipe(h, g, f)h 可以接收多个参数,但 gf 只能接收一个参数。

4. compose vs pipe:选哪个?

既然 composepipe 都能实现函数组合,那么我们应该选择哪个呢?

其实,这取决于个人偏好和代码的可读性。

  • 如果你习惯从右向左阅读代码,或者你的代码逻辑更适合从右向左思考,那么 compose 可能更适合你。
  • 如果你习惯从左向右阅读代码,或者你的代码逻辑更符合数据流动的模式,那么 pipe 可能更适合你。

一般来说,pipe 更符合我们日常的阅读习惯,更容易理解代码的执行流程。 因此,在大多数情况下,我个人更倾向于使用 pipe

5. 实际应用:来点更复杂的例子

理论讲完了,咱们来几个更实际的例子,看看 composepipe 在实际开发中是如何使用的。

5.1 数据转换

假设我们需要从一个包含用户信息的数组中提取所有用户的姓名,并将姓名转换为大写。 可以这样实现:

const users = [
  { id: 1, name: 'john doe', age: 30 },
  { id: 2, name: 'jane smith', age: 25 },
  { id: 3, name: 'peter jones', age: 40 },
];

const getName = user => user.name;
const toUpperCase = str => str.toUpperCase();
const extractNames = users => users.map(getName);

const processNames = pipe(
  extractNames,
  names => names.map(toUpperCase)
);

const result = processNames(users);

console.log(result); // ["JOHN DOE", "JANE SMITH", "PETER JONES"]

在这个例子中,我们使用了 pipe 函数将三个函数 extractNamesnames => names.map(toUpperCase) 组合起来,形成一个新的函数 processNamesprocessNames 函数可以一次性完成提取姓名和转换为大写的功能。

5.2 验证表单

假设我们需要验证一个表单,需要验证用户名、密码和邮箱是否符合规范。 可以这样实现:

const isNotEmpty = str => str.length > 0;
const isEmail = str => /^[^s@]+@[^s@]+.[^s@]+$/.test(str);
const isPasswordValid = str => str.length >= 8;

const validateUsername = username => {
  if (!isNotEmpty(username)) {
    return '用户名不能为空';
  }
  return '';
};

const validatePassword = password => {
  if (!isNotEmpty(password)) {
    return '密码不能为空';
  }
  if (!isPasswordValid(password)) {
    return '密码长度必须大于等于8位';
  }
  return '';
};

const validateEmail = email => {
  if (!isNotEmpty(email)) {
    return '邮箱不能为空';
  }
  if (!isEmail(email)) {
    return '邮箱格式不正确';
  }
  return '';
};

const validateForm = (username, password, email) => {
  return {
    username: validateUsername(username),
    password: validatePassword(password),
    email: validateEmail(email),
  };
};

const form = {
  username: 'john',
  password: 'password123',
  email: '[email protected]',
};

const errors = validateForm(form.username, form.password, form.email);

console.log(errors);

如果使用 compose 或者 pipe,可以把验证逻辑封装得更简洁:

const isNotEmpty = str => str.length > 0;
const isEmail = str => /^[^s@]+@[^s@]+.[^s@]+$/.test(str);
const isPasswordValid = str => str.length >= 8;

const validate = (validator, errorMessage) => value => {
  if (!validator(value)) {
    return errorMessage;
  }
  return '';
};

const validateUsername = validate(isNotEmpty, '用户名不能为空');
const validatePassword = validate(password => isNotEmpty(password) && isPasswordValid(password), '密码不能为空且长度必须大于等于8位');
const validateEmail = validate(isEmail, '邮箱格式不正确');

const form = {
    username: 'john',
    password: 'password123',
    email: '[email protected]',
  };

const errors = {
    username: validateUsername(form.username),
    password: validatePassword(form.password),
    email: validateEmail(form.email)
};

console.log(errors);

这个例子展示了如何使用高阶函数来简化验证逻辑。虽然没有直接使用 composepipe,但这种函数式的思维方式同样可以提高代码的可读性和可维护性。

5.3 Redux Middleware

在 Redux 中,middleware 也是一种函数组合的应用。 Redux middleware 可以拦截和处理 dispatch 的 action,例如:

const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
};

const thunk = store => next => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

applyMiddleware 函数就是用来组合这些 middleware 的:

import { applyMiddleware, createStore } from 'redux';

const store = createStore(reducer, applyMiddleware(thunk, logger));

applyMiddleware 的内部实现其实就是使用 compose 将多个 middleware 组合起来,形成一个新的函数,这个函数可以增强 dispatch 的功能。

6. 小结

今天我们学习了 JavaScript 中 composepipe 这两个非常有用的函数组合工具。 它们可以帮助我们编写更简洁、可复用、易于测试的代码。 掌握 composepipe 是掌握函数式编程的关键一步。

最后,给大家留个小作业:

  1. 尝试自己实现一个 composepipe 函数。
  2. 在自己的项目中找到一些可以使用 composepipe 的场景,并尝试用它们来优化代码。

希望今天的讲座对大家有所帮助! 咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注