Kodokon kodokon.com

函数式模式:柯里化、组合、不可变性

用柯里化、函数组合和不可变数据来组织你的代码,从而获得可预测性和可测试性。

9 分钟 · 3 题

在 Kodokon 中打开本课

JavaScript 把函数当作一等值(first-class values)来对待,这为函数式模式打开了大门。柯里化(currying)把一个 n 元参数的函数变成一连串单参数函数:f(a, b, c) 变成 f(a)(b)(c)。不要把它和部分应用(partial application)混淆,后者在单一步骤中固定住某些参数(fn.bind(null, a))。柯里化在从通用函数创建专用函数方面大放异彩。

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
一个基于函数元数(arity)的通用 curry。

有一个规范上的微妙之处:fn.length 只统计位于第一个默认参数或剩余参数之前的那些参数。(a, b = 1) => {} 的 length 是 1,(...args) => {} 的是 0。因此,一个基于元数的 curry 必须传入参数简单的函数。组合(composition)是下一步:把小的纯函数组装成可读的流水线。

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

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe 从左到右地应用这些函数。

pipe(f, g, h)(x) 计算的是 h(g(f(x))):你按执行顺序来阅读它。compose 变体则从右到左地应用,就像数学记法一样。这些流水线只有在函数是纯的——没有副作用、没有隐藏的依赖——时才有价值,而这就要求不可变数据:你不去修改一个对象,而是生产出它的一个新版本,此时引用相等就足以检测到一次变化(这是 React 协调机制的核心)。

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
通过复制而非修改得到一个新版本。

知识检测

确认你已牢记本课的重点内容。

  1. 柯里化和部分应用之间有什么区别?
    • 没有区别,它们是同义词
    • 柯里化把 f(a, b, c) 变成 f(a)(b)(c);部分应用在单次调用中固定住某些参数
    • 部分应用只对 bind 有效,柯里化只对箭头函数有效
    • 柯里化只适用于两个参数的函数
  2. pipe(f, g, h)(x) 等价于什么?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. const s = Object.freeze({ a: { b: 1 } }) 之后,赋值 s.a.b = 2 能生效吗?
    • 不能,freeze 会冻结整棵对象树
    • 不能,即使在非严格模式下也会抛出 TypeError
    • 能,因为 freeze 是浅层的:嵌套对象 s.a 并没有被冻结
    • 能,但仅在非严格模式下