eslint-plugin-unicorn 的 no-array-reduce 规则全解析:为何禁用Array#reduce()以及如何自动改写为for-of循环
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
no-array-reduce是 eslint-plugin-unicorn 中用于禁止Array#reduce()与Array#reduceRight()调用的 ESLint 规则,它默认开启自动修复(--fix)与编辑器建议(suggestion),能够把常见的reduce调用直接改写成可读性更好的for-of循环,并针对简单求和场景提供Math.sumPrecise()的迁移建议。阅读本文后,你将掌握该规则的禁用动机、选项配置、自动修复边界、Math.sumPrecise()建议的触发条件,以及它在仓库源码与测试中的完整实现细节。
规则概述
no-array-reduce规则的官方描述是 DisallowArray#reduce()andArray#reduceRight(),即禁止一切Array#reduce()与Array#reduceRight()调用。该规则在项目的 readme 规则总表 中被标记为:
- ✅ 在
recommended配置中默认启用(规则源码 config 定义 中docs.recommended: true); - ☑️ 在
unopinionated配置中默认禁用; - 🔧 支持
--fix自动修复(源码meta.fixable: 'code'); - 💡 支持编辑器手动建议(源码
meta.hasSuggestions: true)。
规则元信息中还声明了meta.type: 'suggestion'、defaultOptions: [{allowSimpleOperations: true}],以及languages: ['js/js']。规则在 rules/index.js 中以no-array-reduce为名注册导出。
为什么要禁用 reduce:可读性与性能
原文档指出,Array#reduce()与Array#reduceRight()通常会产出难以阅读且性能更差的代码。在绝大多数场景下,它们都可以被.map、.filter或一个for-of循环取代,后者的意图更直白、更贴近普通的命令式思维。
规则只在一种罕见场景下保留reduce的价值——对数字求和,并且这是默认允许的。若你确实需要reduce,可以使用eslint-disable注释豁免;若你偏好函数式编程风格,也可以直接整体关闭该规则。
基础示例:什么时候报错、什么时候放行
原文档给出了一组完整的正反例,涵盖reduce、reduceRight、.call()/.apply()调用形态:
// ❌ 报错 array.reduce(reducer); // ✅ 放行(通过禁用注释豁免) // eslint-disable-next-line unicorn/no-array-reduce array.reduce(reducer);// ❌ 报错 array.reduce(reducer, initialValue); // ❌ 报错 [].reduce.apply(array, [reducer, initialValue]); // ✅ 推荐写法:for-of + entries() 完整还原 reduce 的四个参数 let result = initialValue; for (const [index, element] of array.entries()) { result = reducer(result, element, index, array); }// ✅ 默认放行:纯数字求和的简单回调 array.reduce((total, value) => total + value);// ❌ 报错 array.reduceRight(reducer, initialValue); // ✅ 推荐写法:从右向左遍历 let result = initialValue; for (let index = array.length - 1; index >= 0; index--) { const element = array[index]; result = reducer(result, element, index, array); }// ❌ 报错:通过 .call() 借用 Array.prototype.reduce [].reduce.call(array, reducer); // ❌ 报错:显式引用 Array.prototype Array.prototype.reduce.call(array, reducer); // ✅ 放行:直接用禁用注释豁免,并改写为直接调用 // eslint-disable-next-line unicorn/no-array-reduce array.reduce(reducer);从源码的 cases 定义 可以确认,规则实际检测三类调用形态:
- 直接调用
array.reduce(...)/array.reduceRight(...):要求参数个数为 1~2 个(minimumArguments: 1、maximumArguments: 2),且第一个参数不是“已知非函数”的值(见isNodeValueNotFunction判断),同时忽略可选链调用(optionalCall: false,因此a?.reduce()与a.reduce?.()不会误报); [].reduce.call(array, ...)/Array.prototype.reduce.call(array, ...)形态(通过 isArrayPrototypeProperty 校验),要求第一个实参不为非函数;[].reduce.apply(array, [...])/Array.prototype.reduce.apply(array, [...])形态(cases 定义)。
测试 test/no-array-reduce.js 中的大量valid用例进一步锁定了边界:a[b.reduce]()、a.reduce()(无参数)、a.reduce(1, 2, 3)、计算属性访问fooreduce、reducex/xreduce这类“形似但非 reduce”的调用,以及第一个实参为数字、字符串、布尔等非函数值的情况都不会被报告。
选项:allowSimpleOperations
规则只有一个选项allowSimpleOperations:
- 类型:
boolean - 默认值:
true - 含义:允许
reduce回调体是单一二元表达式(如加法、减法、乘法等)的简单运算。
该选项在源码的 schema 定义 中被声明为additionalProperties: false的布尔属性,并在 create 函数 中通过const {allowSimpleOperations} = context.options[0]读取。默认值为true,即默认放行简单运算;设置为false则完全禁用reduce。
/* eslint unicorn/no-array-reduce: ["error", {"allowSimpleOperations": true}] */ // ✅ 放行 array.reduce((total, item) => total + item)/* eslint unicorn/no-array-reduce: ["error", {"allowSimpleOperations": false}] */ // ❌ 报错 array.reduce((total, item) => total + item) // ✅ 推荐写法 let total = 0; for (const item of array) { total += item; }从源码的isSimpleOperation判断(rules/no-array-reduce.js#L575-L594)可以看到“简单运算”的精确定义:回调必须是箭头函数或普通函数,且函数体要么直接是一个BinaryExpression(如(total, item) => total + item),要么是只含一条return语句且返回二元表达式的块体(如(total, item) => { return total - item }或function (total, item) { return total * item })。测试中(total / item) * 100这种嵌套二元表达式同样被算作简单运算而默认放行。
特殊建议:迁移到Math.sumPrecise()
当allowSimpleOperations为false时,规则对纯求和(回调为(total, item) => total + item,且无初始值或初始值为字面量0)还会额外提供一个迁移到Math.sumPrecise()的编辑器建议:
/* eslint unicorn/no-array-reduce: ["error", {"allowSimpleOperations": false}] */ // ❌ array.reduce((total, item) => total + item) // ✅(编辑器建议的改写结果) Math.sumPrecise(array)原文档特别强调,这只是一个suggestion(建议)而非 autofix(自动修复),因为Math.sumPrecise()与reduce求和并不完全等价:
- 它要求每个元素都是数字(否则直接抛出异常,而不是隐式强制转换);
- 对空数组返回
-0; - 其数值精度更高,结果可能与逐元素加法不同。
从源码 getSumPreciseSuggestions 的实现可以看到该建议的完整触发与跳过条件:
- 回调必须形如
(accumulator, element) => accumulator + element,且操作数恰好是这两个参数(顺序可交换,即b + a也成立),由isSumReduceCallback(rules/no-array-reduce.js#L59-L88)判定,块体形式{ return a + b; }与function (a, b) { return a + b; }同样支持; - 仅限无初始值或初始值为字面量
0的调用(initialValue.value === 0); - 调用本身不含注释(避免替换时丢失注释);
- 回调参数不能是可证明的非数字类型(借助
isKnownNonNumber的类型推断,例如string[]的字符串拼接场景); - 接收者不能是 BigInt 类型化数组(
BigInt64Array/BigUint64Array,此时Math.sumPrecise()会抛错); - 可选链调用(
array?.reduce(...))不会获得建议。
类型信息的参与:当 TypeScript 类型信息可用时,规则会跳过“可证明非数字”的求和建议。测试 test/no-array-reduce.js#L482-L505 用typescriptEslintParser与projectService覆盖了这些场景:(a: number, b: number) => a + b会获得建议;(a: string, b) => a + b、(a: bigint, b: bigint) => a + b、string[]/boolean[]/bigint[]数组上的求和不会获得建议;而number[]/readonly number[]数组上的求和会获得建议。此外,源码中还通过shouldSkipKnownNonArrayReceiver在类型信息可用时跳过已知的非数组接收者(如Set<number>、Map<string, number>),但已知的数组与类型化数组(如number[]、Int32Array)仍会被正常报告(见 test/no-array-reduce.js#L126-L150)。
值得一提的是,源码注释指出该建议目前仍为手动建议(而非默认自动修复),原因是Math.sumPrecise()尚未进入任何 Node.js 发行版,待其广泛可用后,规则可能会在allowSimpleOperations开启时也报告求和类reduce。
自动修复机制:从reduce到for-of循环
规则会自动修复“常见的直接Array#reduce()调用”,其适用前提是:
- 调用被用作单个变量声明初始化器(
const result = array.reduce(...)),且外层是Program或BlockStatement——由isSingleDeclaratorVariableInitializer(rules/no-array-reduce.js#L27-L37)判定; - 接收者必须是局部
const数组绑定,且声明位置早于该reduce调用; - 回调可以是内联的箭头函数 / 函数表达式,也可以是在调用之前声明、函数体可内联展开的局部
const回调标识符; - 结果变量不得在循环外被继续读取或写入(
hasUnsafeResultReference),数组变量不得在声明与调用之间被写入或读取; - 初始值不得包含副作用(
hasSideEffect),回调本身不得是async/generator、不得写参数、不得使用arguments/this、不得含嵌套method、直接eval、new.target等不安全结构。
修复器由 createFix 生成,它把:
const result = array.reduce((total, item) => transform(total, item), initialValue);改写为(无初始值时会在循环体内用if (index === 0)分支处理首元素):
let result = initialValue; for (const [index, item] of array.entries()) { result = transform(result, item); }上述修复结果在测试 test/no-array-reduce.js#L356-L365 中有完整断言;无初始值的写法(test/no-array-reduce.js#L366-L381)会生成:
const array = []; let result; for (const [index, item] of array.entries()) { if (index === 0) { result = item; continue; } result = transform(result, item); }不会自动修复的复杂情况(仍会报告,但不提供修复):更复杂的回调、Array#reduceRight()、Array#reduce.call()/Array#reduce.apply()、async回调(测试 test/no-array-reduce.js#L152-L155)、带有 TypeScript 类型注解/泛型参数的调用、let array可变绑定、getArray().reduce(...)这类非标识符接收者、回调体内写入数组或结果变量的情况,以及声明附近存在注释的情形。规则为reduceRight生成的提示消息也给出了替代建议:可以先Array#toReversed()再按正向循环处理(见 messages 定义)。
三种报告消息
规则定义了三类报告消息(rules/no-array-reduce.js#L16-L23):
reduce:Array#reduce()is not allowed. Prefer other types of loop for readability.(reduce不允许,建议改用其他循环形式以保证可读性);reduceRight:Array#reduceRight()is not allowed. ... You may want to callArray#toReversed()before looping it.(并提示可先用toReversed()反转数组再循环);sum-precise:Switch toMath.sumPrecise().(迁移建议)。
测试文件通过errorsReduce = [{messageId: 'reduce'}]与errorsReduceRight = [{messageId: 'reduceRight'}]分别断言两类错误,并用 test/snapshots/no-array-reduce.js.md 快照固化Math.sumPrecise()建议的完整输出。
配置与豁免方式
在 ESLint 配置中使用该规则的完整写法如下:
// eslint.config.js(flat config) { rules: { 'unicorn/no-array-reduce': ['error', {allowSimpleOperations: true}], }, }- 想要完全禁用
reduce:将allowSimpleOperations设为false,此时简单求和也会报错,并附带Math.sumPrecise()迁移建议; - 想要豁免个别调用:使用行内
// eslint-disable-next-line unicorn/no-array-reduce或块级/* eslint-disable unicorn/no-array-reduce */注释; - 想要整体关闭:将规则值设为
'off'(例如偏好函数式编程风格的团队)。
小结
no-array-reduce并非单纯“一刀切禁止reduce”的规则:它以可读性与性能为出发点,默认放行简单二元运算(尤其是数字求和),对复杂的reduce/reduceRight/.call()/.apply()调用一律报告,并尽最大努力把安全的直接调用自动改写为语义等价的for-of+entries()循环,同时在allowSimpleOperations: false时对纯求和提供Math.sumPrecise()的类型感知迁移建议。理解其修复边界(哪些情况能修、哪些情况只报告)与选项语义,能帮助你在引入recommended配置后平稳迁移既有代码,避免误伤合理的求和场景。相关实现与验证可继续查阅 规则源码、单元测试 与 快照测试。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考