ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

30 seconds of code 带你入门 JavaScript 函数式编程:纯函数、不可变性与高阶函数实战

30 seconds of code 带你入门 JavaScript 函数式编程:纯函数、不可变性与高阶函数实战 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载函数式编程Functional Programming是一种把计算看作“数学函数求值”、尽量避免更改状态和可变数据的编程范式。本篇文章以 30 seconds of code 仓库中 functional-programming-introduction.md 一文为主体结合仓库内 JavaScript 函数式编程集合functional-programming.yaml及其系列文章完整讲解纯函数、不可变性、高阶函数、递归四大核心概念并延伸覆盖柯里化、函数组合、函数元数、偏应用、记忆化等进阶主题。读完本文你将能够在自己的 JavaScript 项目中识别并写出更易推理、更易测试、可组合性更强的函数式代码。什么是函数式编程函数式编程是一种编程范式programming paradigm。它把计算视为数学函数的求值并尽量避免更改状态与使用可变数据。它强调的是函数的应用与强调状态变化的命令式imperative编程风格形成鲜明对比。在命令式风格中你通常会这样写let total 0; for (let i 0; i numbers.length; i) { total numbers[i]; }代码通过显式地改变total的值来推进计算。而函数式风格更倾向于用数据转换来表达同样的逻辑例如const total numbers.reduce((acc, n) acc n, 0);没有循环中对状态的逐步修改只有“把数组折叠reduce为一个总和”这一纯粹的描述。理解这一思维转变是掌握函数式编程的第一步。核心概念之一纯函数Pure Functions纯函数pure function是指满足以下两个条件的函数给定相同的输入总是返回相同的输出不会在函数作用域之外产生任何副作用side effects。换言之纯函数不会修改任何外部状态或数据它是函数式编程的基石building blocks。原文档给出了最直观的对比示例// 纯函数 const concat (arr, val) [...arr, val]; // 非纯函数 const push (arr, val) arr.push(val);concat通过展开运算符...创建并返回一个新数组原数组保持不变而push直接原地修改传入的数组并返回其长度属于典型副作用。仓库中更详细的讲解见 pure-functions.md它补充了一个非常实用的判断技巧如果一个函数“不用它的返回值调用也有意义”或者“根本不返回任何值”那它通常就是非纯函数——这两类情况都强烈暗示存在副作用。例如// 纯函数 const add (x, y) x y; const order arr [...arr].sort((a, b) a - b); // 先复制再排序 // 非纯函数 const addRandom x x Math.random(); // 相同输入不同输出 const reorder arr arr.sort((a, b) a - b); // 原地修改输入数组纯函数更可靠、可复用也因此更易于推理。在约束允许的前提下应当优先使用纯函数而非非纯函数。核心概念之二不可变性Immutability不可变性immutability意味着值一旦被创建就不能被更改。这正好与可变mutable数据相对——可变数据在创建之后可以被随意修改。不可变性与纯函数紧密相关是函数式编程的另一核心概念。原文档的对比示例非常经典// 可变数据 let arr [1, 2, 3]; arr.push(4); // arr 现在是 [1, 2, 3, 4] // 不可变数据 const otherArr [1, 2, 3]; const newArr [...otherArr, 4]; // newArr 是 [1, 2, 3, 4]otherArr 仍然是 [1, 2, 3]在不可变风格中我们不再修改旧数据而是基于旧数据创建新数据。原数组依然可用程序的其他部分随时可以继续引用它这有助于防止意外的连锁影响。需要特别留意的是immutability.md 中有一条醒目的NOTE上面两个示例中的数组在技术上都是可变的无论用let还是const声明。区别在于第一个示例通过Array.prototype.push()原地修改mutate了数组而第二个示例使用展开运算符创建了新数组、没有修改原数组。也就是说“不可变”的关键不在声明关键字而在于不调用会修改原对象的方法、不直接改动已有数据。[!NOTE]const只保证变量绑定不能被重新赋值并不保证对象内容不可变。若需要真正的深层不可变可参考仓库中的 deep-freeze-object.md 或 immutable-object-proxy.md 等进阶篇目。核心概念之三高阶函数Higher-order Functions高阶函数higher-order function是满足以下任一条件的函数接收函数作为参数返回一个函数。高阶函数在函数式编程中极为常见常被用来构造更复杂的函数。原文档的例子展示了“返回函数”的高阶函数const isEven num num % 2 0; // 高阶函数 const inverse fn (...args) !fn(...args); const isOdd inverse(isEven);inverse接收一个函数fn返回一个新函数——这个新函数对任意参数调用fn后取反。isOdd就是通过组合inverse与isEven得到的新函数。之所以 JavaScript 能写高阶函数是因为函数本身就是值它们可以被赋值给变量、作为参数传递也可以作为返回值。被高阶函数“调用”的参数函数通常被称为回调callback在事件处理、异步代码和数组操作中无处不在。仓库的 higher-order-functions.md 用Array.prototype.filter()和Array.prototype.reduce()展示了最贴近日常的用法const add (a, b) a b; const isEven num num % 2 0; const data [2, 3, 1, 5, 4, 6]; const evenValues data.filter(isEven); // [2, 4, 6] const evenSum data.filter(isEven).reduce(add); // 12filter与reduce本身都是高阶函数它们接收isEven、add作为回调。这样做的好处是为任何动作建立抽象层不必为每种过滤或归约逻辑重写算法本身。事实上30 seconds of code 的大多数代码片段都是围绕高阶函数设计的——它们小而精、高度可复用、易于组合能够拼装出更复杂的逻辑。核心概念之四递归Recursion递归recursion是指函数调用自身的技术。它是函数式编程中解决许多问题的强大工具尤其是在“问题的解依赖于同一个问题更小实例的解”的场景下。原文档以阶乘为例// 递归函数 const factorial num { if (num 0) return 1; return num * factorial(num - 1); };仓库的 recursion.md 进一步给出了斐波那契数列的递归实现并强调了一个关键点——基线条件base caseconst fibonacci n { if (n 1) return n; return fibonacci(n - 1) fibonacci(n - 2); }; fibonacci(6); // 8基线条件是n 1时直接返回n其余情况会把fibonacci拆成n - 1与n - 2两个子调用直到触达基线条件才逐层返回结果。如果没有基线条件函数会无限调用自身最终导致栈溢出stack overflow。原文档与 recursion.md 也提醒读者斐波那契这类问题用递归思路清晰但用迭代往往更高效反过来对于“难以识别和索引子问题”的大量问题递归反而是更自然的选择。递归与迭代的取舍、以及递归性能优化记忆化、改写迭代的完整分析见 recursion-performance-optimization.md我们将在下文进阶部分展开。函数式编程的好处为什么这么多开发者推崇函数式风格原文档给出的核心论据可以总结为四点更易于推理easier to reason about纯函数没有副作用、不可变数据创建后不会变化代码的“因果链”变得清晰可见你只需要关注输入与输出。更易于测试和调试easier to test and debug纯函数无需准备复杂的全局状态给定输入即可断言输出调试时也不会有“某个外部状态悄悄被改了”的谜题。可复用reusable纯函数与数据无耦合同一函数可以在不同上下文、不同模块中直接复用。易于组合easily composed小函数可以像积木一样拼装成更大的程序复杂系统由一批小而简单的构建块搭建而成。此外纯函数天然适合并发与缓存没有共享可变状态就没有数据竞争相同输入必有相同输出使得记忆化memoization成为可能。这些优势将在下面的进阶主题中得到印证。从入门到实战继续你的函数式编程之旅原文档结尾鼓励读者深入阅读其链接的系列文章。该系列正是仓库中journeyId: js/functional-programming这条学习路径入口集合定义见 functional-programming.yaml共收录了 10 篇文章。下面按集合中的顺序逐一简介并给出可直接阅读的仓库路径与核心代码。柯里化Currying柯里化currying把“一个接收多个参数的函数”变换为“一系列各自只接收一个参数的函数”使代码更灵活、更可复用。仓库的 currying.md 给出两版实现。针对参数个数固定的函数可以利用Function.prototype.length获取函数期望的参数个数再结合递归实现const curry (fn) { const curried (...args) ( args.length fn.length ? fn(...args) : (...rest) curried(...args, ...rest) ); return curried; }; const add (x, y) x y; const curriedAdd curry(add); curriedAdd(1)(2); // 3对于可变参数variadic函数如Math.min、Math.powfn.length不再可靠需要显式传入元数arity并可用Function.prototype.bind()支持“预先提供部分参数”即偏应用const curry (fn, arity fn.length, ...args) arity args.length ? fn(...args) : curry.bind(null, fn, arity, ...args); curry(Math.pow)(2)(10); // 1024 curry(Math.min, 3)(10)(50)(2); // 2反向操作uncurry则通过Array.prototype.reduce()逐层调用各柯里化层级参数不足时抛出RangeErrorconst uncurry (fn, arity 1) (...args) { const next acc args args.reduce((x, y) x(y), acc); if (arity args.length) throw new RangeError(Arguments too few!); return next(fn)(args.slice(0, arity)); };函数组合与管道Composition Piping函数组合function composition是把多个函数组合成一个新函数的过程主要收益是可读性、可复用性与模块化。仓库的 function-composition.md 指出与数学一致JavaScript 中的函数组合按从右到左执行const compose (...fns) fns.reduce((f, g) (...args) f(g(...args))); const digitize n [...${n}].map(i parseInt(i)); const add5 x x 5; const multiply (x, y) x * y; const composedFn compose(digitize, add5, multiply); composedFn(5, 2); // [1, 5]如果从右到左不符合直觉可以创建从左到右的pipe()——实现几乎相同只需颠倒调用顺序const pipe (...fns) fns.reduce((f, g) (...args) g(f(...args))); const composedFn pipe(multiply, add5, digitize); composedFn(5, 2); // [1, 5]注意组合时第一个函数可以是任意元数其余函数必须是一元函数。异步场景下的组合技巧可继续阅读 async-function-composition.md。函数元数Function Arity元数arity指一个函数期望接收的参数个数与柯里化、偏应用等技巧配合时非常实用。仓库的 function-arity.md 展示了如何用Function.prototype.length获取元数const arity fn fn.length; arity(Math.sqrt); // 1 arity(Math.pow); // 2 arity((x, y, z) x y z); // 3 arity((...args) args); // 0 —— 可变参数函数元数为 0该文还系统讲解了各类元数限定器arity wrappers// 一元函数只取第一个参数 const unary fn val fn(val); [6, 8, 10].map(unary(Number.parseInt)); // [6, 8, 10] // 二元函数只取前两个参数 const binary fn (a, b) fn(a, b); [2, 1, 0].map(binary(Math.max)); // [2, 1, 2] // n 元函数只取前 n 个参数 const nAry (fn, n) (...args) fn(...args.slice(0, n));以及数组型函数与可变参数函数的互转const toVariadic fn (...args) fn(args); const fromVariadic fn args fn(...args); const allPromises toVariadic(Promise.all.bind(Promise)); let p1 Promise.resolve(1); let p2 Promise.resolve(2); allPromises(p1, p2).then(console.log); // LOGS: [1, 2] const arrayMax fromVariadic(Math.max); arrayMax([1, 2, 3]); // 3unary包装器尤其常用Array.prototype.map会向回调传入 3 个参数Number.parseInt的第二参数恰好是radix直接map(parseInt)会得到错误结果包一层unary就能规避这个经典陷阱。偏应用Partial Application偏应用partial application是把函数的一部分参数固定下来从而产生一个元数更小的新函数。与柯里化不同偏应用一次可以固定任意多个参数而非严格“每次一个”。仓库的 partially-apply-function.md 给出两种方向// 前置参数 const partial (fn, ...partials) (...args) fn(...partials, ...args); const greet (greeting, name) greeting name !; const greetHello partial(greet, Hello); greetHello(John); // Hello John! // 追加参数 const partialRight (fn, ...partials) (...args) fn(...args, ...partials); const greetJohn partialRight(greet, John); greetJohn(Hello); // Hello John!两种实现都依赖展开运算符partial把固定参数放在调用参数之前partialRight则放在之后适用于固定参数位于签名末端的函数。记忆化Memoization记忆化memoization用缓存保存已完成计算的结果从而避免重复执行相同工作显著加速耗时的函数。仓库的 memoization.md 总结了使用记忆化的三条判断标准函数调用慢、开销大或耗时长值得缓存预期在相同条件下多次调用同一函数记忆化加速的是“后续调用”结果存储在内存中因此应避免在“同一函数以差异极大的参数反复调用”的场景使用。基于Map的经典实现如下const memoize fn { const cache new Map(); const cached function (val) { return cache.has(val) ? cache.get(val) : cache.set(val, fn.call(this, val)) cache.get(val); }; cached.cache cache; return cached; }; // 该函数很慢适合记忆化 const anagrams str { if (str.length 2) return str.length 2 ? [str, str[1] str[0]] : [str]; return str .split() .reduce( (acc, letter, i) acc.concat( anagrams(str.slice(0, i) str.slice(i 1)).map(val letter val) ), [] ); }; const anagramsCached memoize(anagrams); anagramsCached(javascript); // 耗时较长 anagramsCached(javascript); // 命中缓存几乎瞬间返回Map之所以合适是因为它以“键值对”保存结果且会记住键的插入顺序。此外JavaScript 的Proxy对象可通过handler.apply()陷阱拦截函数调用、透明地加入缓存是另一种优雅的实现方式见 memoization.md 与 proxy-introduction.md。递归性能优化Recursion Performance Optimization递归代码常因重复计算而低效。仓库的 recursion-performance-optimization.md 以斐波那契为例先用日志展示朴素递归存在大量重复调用再给出两种优化方案。方案一是记忆化——每个n的值只计算一次其余调用全部命中缓存const fibonacciCache new Map(); const fibonacciNumber n { const cacheKey ${n}; if (fibonacciCache.has(cacheKey)) { return fibonacciCache.get(cacheKey); } const r n 2 ? fibonacciNumber(n - 1) fibonacciNumber(n - 2) : n; fibonacciCache.set(cacheKey, r); return r; };方案二是改写为迭代——从小问题迭代到大问题无缓存、无递归调用资源占用更少const fibonacciNumber n { let r 0, l 1, s 0; for (let i 0; i n; i) { r l; l s; s r l; } return s; };该文的结论对任何递归代码都有普适价值迭代更适合“使用频率低”的递归计算而记忆化的缓存可在多次调用之间持续生效更适合“以不同参数反复调用”的场景。优化前务必先想清楚实际使用场景。集合中的其他主题这条学习路径还包含 reorder-function-arguments.md 一篇讲解如何在不改变函数实现的前提下重排函数参数顺序常与柯里化、偏应用组合使用帮助把既有函数的参数顺序调整到组合所需的形式。在仓库中定位这些文章上述所有内容都存放在仓库的 content/snippets/js/s/ 目录下。每篇文章是一个带 YAML frontmatter 的 Markdown 文件例如本文主体 functional-programming-introduction.md 的 frontmatter 包含title: An introduction to functional programming language: javascript tags: [function] listed: true journeyId: js/functional-programmingtags用于按主题归类本文属于functionjourneyId把系列文章串联成一条学习路径集合文件 functional-programming.yaml 中的snippetIds字段按顺序列出该路径下的全部文章 ID是上述“从入门到实战”顺序的直接来源。读者可以按照该集合的snippetIds顺序依次阅读从 functional-programming-introduction.md 入门逐篇掌握纯函数、不可变性、高阶函数、递归、柯里化、函数组合、元数、偏应用与参数重排构建完整的 JavaScript 函数式编程知识体系。小结函数式编程以数学函数求值为模型通过纯函数、不可变性、高阶函数与递归四大核心概念换取更易推理、更易测试、更可复用的代码。30 seconds of code 的这篇入门文章及其所在集合恰好为 JavaScript 开发者提供了一条从概念到实战的完整路径先理解四大概念再进阶到柯里化、函数组合、偏应用、记忆化等工程利器最终写出可组合、可维护的函数式 JavaScript。输出文章赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐30 seconds of Python函数式编程不可变数据与纯函数实践30 seconds of Python函数式编程不可变数据与纯函数实践 你是否在Python开发中遇到过数据状态混乱、函数副作用难以调试的问题本文将通过教程文档clean-code-javascript函数式编程高阶函数与不可变数据的优势clean code javascript函数式编程高阶函数与不可变数据的优势 你是否曾面对这样的困境修改一处代码却在系统另一角落引发难以预料的bug或者文档教程代码质量50projects50days函数式编程纯函数与不可变性50projects50days函数式编程纯函数与不可变性 你还在为JavaScript代码的可维护性发愁吗 当项目规模从hello world膨胀到企示例工程前端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表