一、先明确 Lodash FP 的核心目标与整体架构
1.1 核心目标
把原生 Lodash 从 命令式、data-first(数据在前) 改造成 函数式、iteratee-first(迭代器在前) 的风格,同时满足:
- 自动柯里化:传部分参数返回新函数
- 不可变操作:不修改原数据,返回新数据
- Ramda 兼容性:支持 Ramda 风格的 API 与用法
- 通用性:一套规则适配所有 Lodash 函数,无需硬编码
1.2 整体分层架构与数据流
Lodash FP 采用四层架构,数据流单向流转,规则与逻辑完全分离:
| 层次 | 核心文件 | 核心职责 | 通俗定位 |
|---|---|---|---|
| 配置层 | _mapping.js | 定义所有转换规则(别名、参数、迭代器、特性) | 转换说明书:规定「哪些函数要怎么改」 |
| 核心层 | _baseConvert.js | 实现通用转换逻辑(柯里化、参数重排、不可变包装) | 流水线工人:照着说明书把原生函数改成 FP 版本 |
| 入口层 | _convertBrowser.js | 适配浏览器环境,自动转换全局 Lodash 实例 | 环境适配器:让浏览器能直接用 FP 版本 |
| 打包层 | lib/fp/build-dist.js | Webpack 打包,生成 UMD 格式产物 | 打包工具:产出最终可直接使用的 JS 文件 |
核心数据流:
_mapping.js(规则) → _baseConvert.js(转换) → _convertBrowser.js(适配) → build-dist.js(打包) → lodash.fp.js(产物)
二、配置层 _mapping.js:转换规则的完整定义
_mapping.js 是所有转换的依据,所有规则都围绕 Lodash FP 的核心目标设计,我们按「规则类型 + 作用 + 实现」的逻辑拆解。
2.1 别名映射规则:解决「命名统一 + Ramda 兼容」
为什么定义?
- Lodash 内部有别名(比如
each其实是forEach); - 要兼容 Ramda 生态(比如 Ramda 的
pipe对应 Lodash 的flow); - 统一内部处理逻辑:不管用户写别名还是原名,内部都按真实函数名处理。
怎么定义?
// 1. aliasToReal:别名 → 真实函数名(手动维护)
exports.aliasToReal = {
// Lodash 内部别名
'each': 'forEach', 'first': 'head', 'entries': 'toPairs',
// Ramda 兼容别名
'all': 'every', 'always': 'constant', 'pipe': 'flow', 'compose': 'flowRight'
};
// 2. realToAlias:真实函数名 → 所有别名(自动生成,避免手动维护)
exports.realToAlias = (function() {
const result = {};
for (const alias in exports.aliasToReal) {
const realName = exports.aliasToReal[alias];
(result[realName] || (result[realName] = [])).push(alias);
}
return result;
})();
架构中的作用
- 给核心层提供「别名映射表」:
_baseConvert.js转换时,先通过这个表找到函数的真实名,再应用后续规则。
2.2 参数规则:解决「参数重排 + 自动柯里化」
这是实现 iteratee-first 的关键,分 3 个子规则,规则之间相互配合。
2.2.1 aryMethod:定义函数的参数个数
**为什么定义?**柯里化的前提是「知道函数该接收几个参数」—— 比如 filter 是 2 个参数,柯里化后传 1 个参数(迭代器)就返回新函数,等传第 2 个参数(数据)时才执行。
怎么定义?
exports.aryMethod = {
'1': ['flow', 'floor', 'ceil', 'uniqueId'], // 1个参数的函数
'2': ['filter', 'map', 'forEach', 'includes'], // 2个参数的函数
'3': ['reduce', 'set', 'zipWith', 'getOr'], // 3个参数的函数
'4': ['fill', 'setWith'] // 4个参数的函数
};
2.2.2 aryRearg:默认参数重排规则
**为什么定义?**统一规定「不同参数个数的函数,参数顺序怎么调整」,核心是把 data-first 改成 iteratee-first。
怎么定义?
exports.aryRearg = {
'2': [1, 0], // 2参数函数:参数反转 → 迭代器在前,数据在后
'3': [2, 0, 1], // 3参数函数:第三个参数移到首位 → 迭代器在前
'4': [3, 2, 0, 1] // 4参数函数:第四个参数移到首位
};
例子:原生 _.filter(data, iteratee) 是 2 参数函数,按 [1,0] 反转后,变成 fp.filter(iteratee, data)。
2.2.3 methodRearg:特殊函数的自定义重排规则
**为什么定义?**有些函数的参数逻辑特殊,不能用默认规则 —— 比如 getOr 的参数是 (data, path, defaultValue),默认的 3 参数规则不适用,需要自定义。
怎么定义?
exports.methodRearg = {
'getOr': [2, 1, 0], // getOr → (defaultValue, path, data)
'isMatchWith': [2, 1, 0],
'zipWith': [1, 2, 0]
};
架构中的作用
- 给核心层提供「参数处理依据」:
_baseConvert.js先通过aryMethod确定参数个数,再通过aryRearg/methodRearg调整参数顺序,最后基于参数个数做柯里化。
2.3 迭代器规则:解决「简化迭代器参数」
为什么定义?
原生 Lodash 的迭代器会传冗余参数 —— 比如 map 的迭代器默认传 (val, idx, arr),但函数式编程中 90% 的场景只需要 val,简化后更易用。
怎么定义?
// 1. iterateeAry:定义迭代器接收的参数个数
exports.iterateeAry = {
'map': 1, 'filter': 1, 'forEach': 1, // 迭代器只接收 val
'reduce': 2, 'reduceRight': 2 // 迭代器接收 acc 和 val
};
// 2. iterateeRearg:特殊迭代器的参数重排
exports.iterateeRearg = {
'mapKeys': [1], // mapKeys 的迭代器只接收 key
'reduceRight': [1, 0] // reduceRight 迭代器参数反转
};
架构中的作用
- 给核心层提供「迭代器简化规则」:
_baseConvert.js基于这个规则,把迭代器的参数个数限制到指定数量。
2.4 特性规则:解决「不可变 + 特殊行为」
2.4.1 mutate:标记「修改原数据的函数」
**为什么定义?**函数式编程要求「纯函数」,不能修改原数据。需要标记这些函数,让核心层对它们做「克隆后修改」的包装。
怎么定义?
exports.mutate = {
'array': { 'fill': true, 'pull': true, 'reverse': true },
'object': { 'assign': true, 'merge': true, 'defaults': true },
'set': { 'set': true, 'unset': true, 'update': true }
};
2.4.2 skipFixed/skipRearg:标记「不适用默认规则的函数」
**为什么定义?**有些函数的逻辑特殊,不能限制参数个数或重排参数 —— 比如 flow 可以传任意多个函数,add 的参数顺序没必要反转。
怎么定义?
// skipFixed:不限制参数个数
exports.skipFixed = { 'flow': true, 'mixin': true, 'runInContext': true };
// skipRearg:不重排参数顺序
exports.skipRearg = { 'add': true, 'eq': true, 'merge': true };
架构中的作用
- 给核心层提供「特殊函数处理依据」:
_baseConvert.js会跳过这些函数的默认规则,避免转换出错。
三、核心层 _baseConvert.js:通用转换逻辑的实现
_baseConvert.js 的核心是 baseConvert 函数,它的作用是:读取 _mapping.js 的规则,对单个原生 Lodash 函数执行「标准化转换流程」。
我们以「转换 filter 函数」为例,结合架构数据流,拆解完整的转换步骤。
3.1 转换的前置准备:初始化配置
首先定义转换的「开关」,可以通过 options 自定义(比如关闭柯里化):
function baseConvert(util, name, func, options) {
// 配置开关:默认开启所有核心特性
const config = {
curry: options.curry ?? true, // 自动柯里化
rearg: options.rearg ?? true, // 参数重排
immutable: options.immutable ?? true, // 不可变包装
cap: options.cap ?? true // 限制迭代器参数个数
};
// 核心转换函数:wrap 是真正处理函数的地方
return wrap(name, func, util.placeholder);
}
3.2 核心转换流程:wrap 函数的 6 个步骤
wrap 函数是转换的核心,所有规则都会在这里落地,步骤是固定的,适用于所有 Lodash 函数。
步骤 1:解析真实函数名(应用别名规则)
function wrap(name, func, placeholder) {
// 从 aliasToReal 找到真实函数名:比如 name 是 'each' → 真实名是 'forEach'
const realName = mapping.aliasToReal[name] || name;
let wrapped = func; // 初始化 wrapped 为原生函数
步骤 2:不可变包装(应用 mutate 规则)
如果函数在 mutate 列表中(比如 set),就用 wrapImmutable 包装,实现「先克隆、再修改」:
if (config.immutable) {
// 判断函数类型:数组/对象/set
if (mapping.mutate.array[realName]) {
wrapped = wrapImmutable(func, cloneArray); // 克隆数组
} else if (mapping.mutate.object[realName]) {
wrapped = wrapImmutable(func, createCloner(func)); // 克隆对象
}
}
// wrapImmutable 的核心逻辑
function wrapImmutable(func, cloner) {
return function(...args) {
const clonedData = cloner(args[0]); // 克隆原数据
args[0] = clonedData; // 替换成克隆后的数据
func(...args); // 修改克隆后的数据(原数据不变)
return clonedData; // 返回新数据
};
}
对于 filter:它不在 mutate 列表中,跳过这一步。
步骤 3:参数个数限制(应用 aryMethod 规则)
从 aryMethod 找到函数的参数个数,固定参数个数(比如 filter 是 2 个参数):
// 遍历 aryMethod,找到当前函数的参数个数
for (const aryKey of Object.keys(mapping.aryMethod)) {
if (mapping.aryMethod[aryKey].includes(realName)) {
// castFixed:固定参数个数 → filter 只能接收 2 个参数
wrapped = castFixed(realName, wrapped, aryKey);
break;
}
}
// castFixed 核心逻辑
function castFixed(name, func, n) {
if (config.fixed && !mapping.skipFixed[name]) {
return _.ary(func, n); // 用 Lodash 内置的 ary 函数限制参数个数
}
return func;
}
步骤 4:参数顺序重排(应用 aryRearg/methodRearg 规则)
按规则调整参数顺序,实现 iteratee-first:
// castRearg:重排参数 → filter 按 [1,0] 反转
wrapped = castRearg(realName, wrapped, aryKey);
// castRearg 核心逻辑
function castRearg(name, func, n) {
if (config.rearg && !mapping.skipRearg[name]) {
// 优先用自定义规则 methodRearg,没有就用默认规则 aryRearg
const rule = mapping.methodRearg[name] || mapping.aryRearg[n];
return _.rearg(func, rule); // 用 Lodash 内置的 rearg 重排参数
}
return func;
}
对于 filter:原生 (data, iteratee) → 重排后 (iteratee, data)。
步骤 5:迭代器参数简化(应用 iterateeAry 规则)
限制迭代器的参数个数,简化用户的使用成本:
// castCap:限制迭代器参数个数 → filter 的迭代器只接收 1 个参数(val)
wrapped = castCap(realName, wrapped);
// castCap 核心逻辑
function castCap(name, func) {
if (config.cap) {
const iterateeN = mapping.iterateeAry[name];
if (iterateeN) {
return overArg(func, (iter) => _.ary(iter, iterateeN));
}
}
return func;
}
对于 filter:迭代器从接收 (val, idx, arr) 变成只接收 (val)。
步骤 6:自动柯里化(应用 aryMethod 规则)
基于参数个数做柯里化,让函数支持部分应用:
// castCurry:柯里化 → filter 是 2 参数函数,传 1 个参数返回新函数
wrapped = castCurry(realName, wrapped, aryKey);
// castCurry 核心逻辑
function castCurry(name, func, n) {
if (config.curry && n > 1) {
return _.curry(func, n); // 用 Lodash 内置的 curry 函数柯里化
}
return func;
}
// 给转换后的函数加属性:支持占位符和自定义转换
wrapped.placeholder = placeholder;
wrapped.convert = createConverter(realName, func);
return wrapped;
}
3.3 转换后的效果:filter 函数的前后对比
| 特性 | 原生 Lodash | Lodash FP(转换后) | 依赖的规则 |
|---|---|---|---|
| 参数顺序 | _.filter(data, iter) | fp.filter(iter, data) | aryRearg: [1,0] |
| 柯里化 | 需手动 _.curry(_.filter) | 自动柯里化:fp.filter(iter)(data) | aryMethod: '2' |
| 迭代器 | 接收 val, idx, arr | 只接收 val | iterateeAry: 1 |
| 不可变 | 无(不修改原数据) | 无(无需包装) | mutate 无 filter |
| 别名 | 无 each 别名 | fp.each 等价于 fp.forEach | aliasToReal: each→forEach |
3.4 架构中的作用
_baseConvert.js 是配置层和入口层的桥梁:它读取配置层的规则,输出转换后的 FP 函数,再交给入口层做环境适配。
四、入口层与打包层:从转换到可用产物
4.1 入口层 _convertBrowser.js:浏览器环境适配
_convertBrowser.js 是对 baseConvert 的简单包装,核心作用是 自动转换浏览器全局的 Lodash 实例:
const baseConvert = require('./_baseConvert');
function browserConvert(lodash, options) {
// 转换整个 Lodash 库:把所有原生函数都变成 FP 版本
return baseConvert(lodash, lodash, options);
}
// 自动转换全局 _ 对象:浏览器引入 Lodash 后,直接用 _ 就是 FP 版本
if (typeof _ === 'function' && typeof _.runInContext === 'function') {
_ = browserConvert(_.runInContext());
}
module.exports = browserConvert;
4.2 打包层 build-dist.js:生成最终产物
打包层基于 Webpack,把转换后的代码打包成 UMD 格式(支持浏览器、Node.js、模块化引入),核心流程:
- 打包
_mapping.js生成mapping.fp.js(规则文件); - 打包
_convertBrowser.js生成lodash.fp.js(核心产物); - 压缩
lodash.fp.js生成lodash.fp.min.js(生产环境用)。
打包配置核心代码:
const webpack = require('webpack');
const fpConfig = {
entry: './fp/_convertBrowser.js',
output: {
path: './dist',
filename: 'lodash.fp.js',
library: 'fp',
libraryTarget: 'umd' // 支持所有模块系统
}
};
// 执行打包
async.series([
(cb) => webpack(fpConfig, cb),
(cb) => file.min('./dist/lodash.fp.js', cb)
]);
五、全局逻辑串联总结
- 配置层
_mapping.js定义「转换说明书」:规定每个函数的参数、迭代器、不可变等规则; - 核心层
_baseConvert.js是「流水线工人」:照着说明书,对每个原生函数执行「找真实名→不可变包装→参数处理→迭代器简化→柯里化」的固定流程; - 入口层
_convertBrowser.js是「环境适配器」:把转换后的函数适配到浏览器环境; - 打包层 是「打包工具」:把所有代码打包成 UMD 格式的产物,供开发者直接使用。
整个流程的核心是 「规则与逻辑分离」—— 新增或修改函数的转换方式,只需要改 _mapping.js 的规则,不需要动核心转换逻辑,这也是 Lodash FP 扩展性强的根本原因。