es-toolkit/compat 的 range 函数完全指南:Lodash 互換の数値範囲生成を徹底解説
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
本篇技术指南聚焦于 es-toolkit 的 Lodash 兼容入口es-toolkit/compat中的range函数(日文文档:docs/ja/compat/reference/math/range.md)。你将掌握range的三种调用签名(单参、双参、三参)、自动步长推断、零步长与小步步长的边界行为、以及作为map等函数 iteratee 的 guard 用法,并深入源码层理解其参数归一化(toFinite)与迭代调用识别(isIterateeCall)的底层实现,最终学会在 Lodash 迁移场景中正确选型。
一、函数定位:Lodash 兼容的range
es-toolkit/compat是 es-toolkit 为平滑迁移 Lodash 而提供的兼容入口。其中的range函数用于创建数值范围数组,行为与_.range保持一致:
const numbers = range(start, end, step);需要特别注意的是,该 compat 版本在 文档开头即给出警告:由于它需要处理复杂的参数形态与类型转换(如字符串数字、NaN、Infinity、假值等),运行速度比 es-toolkit 原生range更慢。文档明确建议:追求性能时应改用更快速、更现代的 es-toolkit 原生 range(从es-toolkit/math导入)。换句话说,compat版本的存在价值是"行为兼容优先",而非"性能优先"。
二、三种调用签名与基本用法
1.range(end):从 0 开始、步长为 1
只传一个参数时,end被当作范围的终点(不含),起点固定为 0,步长自动为 1:
import { range } from 'es-toolkit/compat'; range(4); // Returns: [0, 1, 2, 3] range(0); // Returns: [] range(-4); // Returns: [0, -1, -2, -3]可以看到,当end为负数时,步长会自动推断为 -1,向负方向生成序列。当end为 0 时返回空数组。
2.range(start, end):从 start 到 end,自动推断步长
传入起止两个参数时,步长根据start < end自动确定为 1 或 -1,无需手动指定方向:
import { range } from 'es-toolkit/compat'; range(1, 5); // Returns: [1, 2, 3, 4] range(5, 1); // Returns: [5, 4, 3, 2] (自動的に-1ずつ減少) range(-2, 3); // Returns: [-2, -1, 0, 1, 2]起点包含在结果中,终点不包含。
3.range(start, end, step):显式指定步长
三参数形态完全由你控制步长,支持正负方向、零步长以及小数步长:
import { range } from 'es-toolkit/compat'; range(0, 20, 5); // Returns: [0, 5, 10, 15] range(0, -4, -1); // Returns: [0, -1, -2, -3] range(1, 4, 0); // Returns: [1, 1, 1]其中step = 0是一个值得注意的 Lodash 兼容行为:结果数组中会不断重复start的值(循环次数由(end - start) / 1向上取整决定)。这与原生range(见第四节)会直接抛错的行为截然不同。
小数步长同样被支持:
import { range } from 'es-toolkit/compat'; range(0, 1, 0.2); // Returns: [0, 0.2, 0.4, 0.6, 0.8] range(1, 0, -0.25); // Returns: [1, 0.75, 0.5, 0.25]4. 参数与返回值一览
| 参数 | 类型 | 说明 |
|---|---|---|
start | number | 范围的起始值(包含)。当不传end时,该值被当作end使用 |
end | number(可选) | 范围的结束值(不包含) |
step | number(可选) | 递增/递减步长,默认根据方向取 1 或 -1 |
返回值:number[],即按指定范围与步长生成的数值数组。
三、作为 iteratee 使用:guard 参数机制
range还可以直接作为map等方法的回调(iteratee)使用,此时会自动命中 guard 分支:
import { range } from 'es-toolkit/compat'; [1, 2, 3].map(range); // Returns: [[0], [0, 1], [0, 1, 2]]其原理在源码 src/compat/math/range.ts 中清晰可见——实现为函数重载:当传入三个参数(end, index, guard)时,若step存在但不是数字且满足isIterateeCall(start, end, step),则判定为 iteratee 调用,将end与step重置为undefined,从而退回"仅end"的处理路径:
export function range(start: number, end?: PropertyKey, step?: any): number[] { // Enables use as an iteratee for methods like `_.map`. if (step && typeof step !== 'number' && isIterateeCall(start, end, step)) { end = step = undefined; } // ... }isIterateeCall定义在 src/compat/_internal/isIterateeCall.ts,其判定逻辑是:
- 第三个参数
object必须是一个对象(isObject); - 若第二个参数
index是数字,则它必须是该对象(类数组)的有效索引(借助 isIndex 校验非负整数且小于长度); - 若
index是字符串,则它必须是对象上的属性; - 最后校验
object[index] === value(通过eq比较)。
只有在以上条件全部满足时才认定是一次 iteratee 调用。对应的测试在 src/compat/math/range.spec.ts 中验证了数组与对象两种集合map场景均能得到[[0], [0, 1], [0, 1, 2]]。
四、源码级剖析:compat 版为何"慢"而"兼容"
1. 参数归一化:toFinite
compat 版的第一步是把所有参数强制转为有限数字,调用 src/compat/util/toFinite.ts:
start = toFinite(start); if (end === undefined) { end = start; start = 0; } else { end = toFinite(end); } step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);toFinite的处理规则包括:
- 假值(
null、undefined、''、false、0)统一归一为 0(-0会被保留); - 字符串数字(如
'1')经toNumber转为数字; Infinity/-Infinity被钳制为Number.MAX_VALUE的正负值;NaN归一为 0。
这正是测试 src/compat/math/range.spec.ts 中断言的range('1')、range('0', 1)、range(0, 1, '1')均能给出合理结果的原因——它们分别返回[[0], [0], [0]],而range(NaN)与range(NaN, NaN)返回空数组。相比之下,原生range不做这些转换,因此更快。
2. 长度计算与数组填充
步长确定后,结果长度通过如下公式计算:
const length = Math.max(Math.ceil((end - start) / (step || 1)), 0); const result = new Array(length); for (let index = 0; index < length; index++) { result[index] = start; start += step; } return result;几个关键点:
step || 1使得step = 0时按步长 1 计算长度,从而产生重复start的序列(对应range(1, 4, 0)返回[1, 1, 1]);- 当
step绝对值大于区间跨度时,Math.ceil后长度可能为 1(测试 range.spec.ts 验证range(1, 5, 20)返回[1]); - 负向区间配合负步长同样成立,如
range(21, 10, -3)返回[21, 18, 15, 12]; - 使用
new Array(length)预分配容量,避免逐个push带来的扩容开销; -0起始值会被保留(测试 range.spec.ts 验证1 / actual[0]为-Infinity)。
五、与原生range的关键差异与选型建议
es-toolkit 原生range(src/math/range.ts)与 compat 版本在 API 相似但语义上有明确区别:
| 对比项 | compatrange | 原生range |
|---|---|---|
| 导入路径 | es-toolkit/compat | es-toolkit/math(见 文档) |
| 参数转换 | 经toFinite接受字符串、NaN等 | 直接使用数字 |
step = 0 | 返回重复start的数组 | 抛出错误 |
| 小数步长 | 支持 | 要求非零整数,否则抛错 |
| 性能 | 因复杂参数处理与类型转换而较慢 | 更快速、更现代 |
原生实现 src/math/range.ts 中明确通过Number.isInteger(step) || step === 0校验并抛出The step value must be a non-zero integer.,同时默认步长为 1(不根据方向自动推断为 -1),元素生成采用start + i * step的乘法累加方式,不做 iteratee guard 处理。
选型建议:
- 正在从 Lodash 迁移、需要"一行不改"地保持既有
_.range行为(含零步长、字符串参数等历史怪癖)时,使用es-toolkit/compat版本; - 新写代码、对性能敏感(例如在热循环中生成索引序列)时,优先使用
es-toolkit/math的原生range,并遵守其"step 必须为非零整数"的约束; - 日常生成连续索引(
range(10)得到[0..9])或分页序号(range(1, 11)得到1..10)时,两种版本均可胜任,此时原生版本是更优选择。
六、行为边界速查
结合 源码 与 测试用例,以下是 compat 版range的完整行为边界清单:
- 单参负数:
range(-4)→[0, -1, -2, -3],步长自动为 -1; - 双参降序:
range(5, 1)→[5, 4, 3, 2],无需显式负步长; - 零步长:
range(1, 4, 0)→[1, 1, 1]; - 步长超过区间:
range(1, 5, 20)→[1]; - 负向大步长:
range(21, 10, -3)→[21, 18, 15, 12]; - 假值 start:
range()、range(null)等均按 0 处理(range()返回[]); - 类型强制:
range('1')→[0],range('0', 1)→[0],range(0, 1, '1')→[0],range(NaN)→[]; - iteratee:
[1, 2, 3].map(range)→[[0], [0, 1], [0, 1, 2]],且对数组与普通对象集合均生效。
掌握这些边界行为,你就能在 Lodash 迁移与日常开发中准确预判range的输出,并在需要更高性能时果断切换到原生版本。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考