阅读时间:约 10 分钟
适合人群: 具备 JavaScript 基础、想深入理解函数式编程与高阶函数实战的前端工程师。本文目标:
- 了解如何在没有函数名的情况下实现递归
- 掌握构建通用递归生成器的思路
- 学会在真实项目中使用高阶函数递归处理复杂逻辑
从匿名递归开始
先看一段看似难以理解的代码:
scss
(g => g(g))(f => n => n < 2 ? 1 : n * f(f)(n - 1))(6);
执行结果是:
720
即 6!(6 的阶乘(factorial of 6))。
在数学中,阶乘(factorial) 是指一个正整数与它所有比它小的正整数的乘积:
<math xmlns="http://www.w3.org/1998/Math/MathML"> n ! = n × ( n − 1 ) × ( n − 2 ) × ⋯ × 1 n! = n \times (n - 1) \times (n - 2) \times \cdots \times 1 </math>n!=n×(n−1)×(n−2)×⋯×1
拆解原理:
(f => n => n < 2 ? 1 : n * f(f)(n - 1)):定义阶乘逻辑;(g => g(g)):自应用函数(Self-Application Function) ;(g => g(g))(f => ...):将函数传入自身,让匿名函数内部实现递归。
这种方式实现了没有名字的递归函数。
提取为可复用的函数
为了可读性更高,我们提取一个通用的封装函数:
ini
const fn = (f) => {
return (g => g(g))(f);
};
const fn1 = fn(f => (n, acc = 1) => n === 0 ? acc : f(f)(n - 1, n * acc));
console.log(fn1(6)); // 720
fn() 封装了匿名递归结构,传入的函数只需要关心逻辑。
构建通用的递归函数生成器
我们进一步封装成更通用的写法:
javascript
const createRecursiveFunction = (f) => {
return (g => g(g))(h => f((...args) => h(h)(...args)));
};
解释:
f是接收self参数的函数;self表示递归自身;(g => g(g))让函数可以引用自身。
例 1:阶乘(factorial)
ini
const factorialLogic = (self) => (n, acc = 1) => {
return n === 0 ? acc : self(n - 1, n * acc);
};
const factorial = createRecursiveFunction(factorialLogic);
console.log(factorial(6)); // 720
这是一种尾递归写法,性能更优。
例 2:斐波那契数列(fibonacci)
ini
const fibonacciLogic = (self) => (n) => {
return n <= 1 ? n : self(n - 1) + self(n - 2);
};
const fibonacci = createRecursiveFunction(fibonacciLogic);
console.log(fibonacci(6)); // 8
例 3:爬楼梯问题(climbStairs)
每次可以爬 1 或 2 阶,问总共有多少种爬法?
ini
const climbStairsLogic = (self) => n => {
return n <= 2 ? n : self(n - 1) + self(n - 2);
};
const climbStairs = createRecursiveFunction(climbStairsLogic);
console.log(climbStairs(6)); // 13
例 4:棋盘麦粒问题(chessGrains)
棋盘第一个格子放 1 颗麦子,每下一个格子是前一个的 2 倍。问第 64 个格子上有多少颗?
ini
const chessGrainsLogic = (self) => (n, acc = 1) => {
return n === 0 ? acc : self(n - 1, acc * 2);
};
const chessGrains = createRecursiveFunction(chessGrainsLogic);
console.log(chessGrains(63)); // 9223372036854775808
完整代码
scss
const createRecursiveFunction = (f) => {
return (g => g(g))(h => f((...args) => h(h)(...args)));
};
// 阶乘
const factorialLogic = (self) => (n, acc = 1) =>
n === 0 ? acc : self(n - 1, n * acc);
const factorial = createRecursiveFunction(factorialLogic);
console.log('factorial(6) =', factorial(6));
// 斐波那契
const fibonacciLogic = (self) => (n) =>
n <= 1 ? n : self(n - 1) + self(n - 2);
const fibonacci = createRecursiveFunction(fibonacciLogic);
console.log('fibonacci(6) =', fibonacci(6));
// 爬楼梯
const climbStairsLogic = (self) => n =>
n <= 2 ? n : self(n - 1) + self(n - 2);
const climbStairs = createRecursiveFunction(climbStairsLogic);
console.log('climbStairs(6) =', climbStairs(6));
// 棋盘麦粒
const chessGrainsLogic = (self) => (n, acc = 1) =>
n === 0 ? acc : self(n - 1, acc * 2);
const chessGrains = createRecursiveFunction(chessGrainsLogic);
console.log('chessGrains(63) =', chessGrains(63));
总结:高阶函数的力量
| 概念 | 含义 |
|---|---|
| 高阶函数 | 接收或返回函数的函数 |
| 匿名递归 | 无需函数名即可递归 |
自应用函数 (g => g(g)) |
实现匿名自引用的关键 |
| 通用递归生成器 | 可动态生成任意递归逻辑 |
思考练习
尝试使用 createRecursiveFunction 实现以下函数:
- 非尾递归版阶乘
- 求数组和
- 二分查找
好的,我帮你把这一节整理成 掘金文章的新一章,保持和前文一致的 Markdown 风格,标题、代码、说明都完整,方便直接发布。
十、递归练习与思维扩展
在前文中,我们学习了如何用高阶函数和通用递归生成器 createRecursiveFunction 构建各种递归函数。
为了加深理解,可以尝试一些练习题,实践不同类型的递归逻辑。
本章给出三个示例:
- 非尾递归版阶乘
- 求数组和
- 二分查找
1. 非尾递归版阶乘
非尾递归的特点是递归调用不是最后一步,返回值还要参与计算。
ini
const factorialLogic = (self) => (n) => {
if (n === 0) return 1;
return n * self(n - 1); // 非尾递归
};
const factorial = createRecursiveFunction(factorialLogic);
console.log(factorial(6)); // 输出 720
解析:
- 每次递归返回一个值参与乘法计算;
- 调用栈会随着
n增大而增加。
2. 求数组和
利用递归求数组所有元素的和。
ini
const sumArrayLogic = (self) => (arr) => {
if (arr.length === 0) return 0;
const [head, ...tail] = arr;
return head + self(tail); // 非尾递归
};
const sumArray = createRecursiveFunction(sumArrayLogic);
console.log(sumArray([1, 2, 3, 4, 5])); // 输出 15
解析:
- 每次取数组的第一个元素
head,递归计算剩余元素tail的和;- 递归调用返回值与
head相加得到最终结果。
3. 二分查找
在有序数组中高效查找目标元素索引。
ini
const binarySearchLogic = (self) => (arr, target, left = 0, right = arr.length - 1) => {
if (left > right) return -1; // 未找到
const mid = Math.floor((left + right) / 2);
const value = arr[mid];
if (value === target) return mid;
if (value > target) return self(arr, target, left, mid - 1);
return self(arr, target, mid + 1, right);
};
const binarySearch = createRecursiveFunction(binarySearchLogic);
const arr = [1, 3, 5, 7, 9, 11, 13];
console.log(binarySearch(arr, 7)); // 输出 3
console.log(binarySearch(arr, 4)); // 输出 -1
解析:
- 每次递归缩小查找范围;
- 递归结束条件是
left > right;- 返回目标元素索引或 -1。
特征
| 函数 | 类型 | 递归特点 |
|---|---|---|
| 阶乘 | 非尾递归 | 调用后还有乘法操作 |
| 数组求和 | 非尾递归 | 每次取头递尾 |
| 二分查找 | 非尾递归 | 分治思想,递归缩小范围 |
核心思路
- 只需提供递归逻辑,
createRecursiveFunction负责自引用; - 递归逻辑模板统一:
javascript
const logic = (self) => (...args) => {
// ...递归逻辑
return self(...args); // 调用自身
};
const fn = createRecursiveFunction(logic);
通过练习,可以更好理解高阶函数 + 递归的组合思维,并能应用于实际项目中的树结构、异步任务等场景。
项目中的实际应用场景
在真实项目中,高阶函数递归能让代码更灵活、更模块化。
常见的实战场景包括:
- 树结构遍历(菜单、评论、权限等)
- 异步重试机制(API 调用失败重试)
- 动态轮询(等待异步任务完成)
遍历树形菜单
ini
const createRecursiveFunction = (f) => {
return (g => g(g))(h => f((...args) => h(h)(...args)));
};
// 扁平化树
const flattenTreeLogic = (self) => (nodes, result = []) => {
for (const node of nodes) {
result.push({ id: node.id, name: node.name });
if (node.children?.length) self(node.children, result);
}
return result;
};
const flattenTree = createRecursiveFunction(flattenTreeLogic);
const menu = [
{ id: 1, name: '首页' },
{
id: 2,
name: '用户管理',
children: [
{ id: 3, name: '用户列表' },
{ id: 4, name: '用户详情' },
],
},
];
console.log(flattenTree(menu));
输出:
bash
[
{ id: 1, name: '首页' },
{ id: 2, name: '用户管理' },
{ id: 3, name: '用户列表' },
{ id: 4, name: '用户详情' }
]
异步重试机制
javascript
const createRecursiveFunction = (f) => {
return (g => g(g))(h => f((...args) => h(h)(...args)));
};
const retryLogic = (self) => async (fn, retries = 3, delay = 1000) => {
try {
return await fn();
} catch (err) {
if (retries <= 0) throw err;
console.log(`重试中,还剩 ${retries} 次...`);
await new Promise(r => setTimeout(r, delay));
return self(fn, retries - 1, delay);
}
};
const retry = createRecursiveFunction(retryLogic);
let count = 0;
const unstableApi = async () => {
count++;
if (count < 3) throw new Error('失败');
return '成功';
};
retry(unstableApi, 5, 500)
.then(res => console.log('结果:', res))
.catch(err => console.error('最终失败:', err.message));
输出:
erlang
重试中,还剩 5 次...
重试中,还剩 4 次...
结果: 成功
动态轮询任务
javascript
const createRecursiveFunction = (f) => {
return (g => g(g))(h => f((...args) => h(h)(...args)));
};
const pollingLogic = (self) => async (checkFn, interval = 1000, maxTries = 10) => {
const done = await checkFn();
if (done) return true;
if (maxTries <= 0) return false;
console.log(`未完成,${interval}ms 后再次检查...`);
await new Promise(r => setTimeout(r, interval));
return self(checkFn, interval, maxTries - 1);
};
const polling = createRecursiveFunction(pollingLogic);
let progress = 0;
const checkTask = async () => {
progress += 30;
return progress >= 100;
};
polling(checkTask, 1000, 5).then(done => {
console.log(done ? '任务完成!' : '超时未完成');
});
从抽象到实用
| 场景 | 应用优势 |
|---|---|
| 树结构处理 | 灵活定义逻辑,便于复用 |
| 异步重试 | 自动化错误恢复 |
| 动态轮询 | 可配置、可扩展 |
高阶函数递归不仅是理论技巧,更是函数式思维在工程化中的体现。
当逻辑复杂、变化频繁时,用函数组合取代命名函数,可以极大提升灵活性。
结语
本文从匿名递归出发,一步步构建了一个通用的递归生成器,并结合项目实战展示了高阶函数递归的实用价值。
这类模式的核心思想是:
将递归结构与逻辑解耦,让函数更通用、更可组合。
如果你觉得本文对你有帮助,欢迎点赞、收藏或分享,让更多人了解高阶函数的实际应用。
你的点赞,就是我继续写作的最大动力。