eslint no-param-reassign 规则详解:禁止修改函数参数,从配置到源码实现
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
本指南以 eslint 官方规则文档 no-param-reassign.md 为核心,系统讲解该规则的设计动机、props/ignorePropertyModificationsFor/ignorePropertyModificationsForRegex三个配置项的完整用法,并结合本仓库源码 lib/rules/no-param-reassign.js 与测试用例 tests/lib/rules/no-param-reassign.js 剖析其内部判定逻辑。读完本文,你将掌握该规则的配置方法、边界行为与底层实现原理,能够在自己的项目中准确启用并定制它。
规则动机:为什么不应重新赋值函数参数
对函数参数的重新赋值容易造成误导并引发令人困惑的行为。在非strict模式下,修改函数参数会同步变更arguments对象中的索引值,导致函数内部与调用方对参数状态的认知出现偏差(该行为在strict模式下的影响,见下文 何时不使用此规则 小节)。多数情况下,对参数赋值并非开发者本意,而是笔误或逻辑错误的信号。
除重新赋值外,该规则还可配置为在函数参数被修改(包括其属性被修改)时同样报告错误。参数上的副作用会产生反直觉的执行流程,让错误变得难以定位。因此,该规则被归类为suggestion类型(可在 conf/rule-type-list.json 的类型清单中查看该类型定义),用于在编码阶段提前暴露潜在的参数滥用。
Rule Details:规则检查什么
该规则的目标是阻止因修改或重新赋值函数参数而引发的非预期行为。其默认行为(不配置任何选项时)覆盖以下四类"直接写回参数"的写法,均为错误代码:
/*eslint no-param-reassign: "error"*/ const foo = function(bar) { bar = 13; // 直接赋值 } const foo1 = function(bar) { bar++; // 自增/自减(UpdateExpression) } const foo2 = function(bar) { for (bar in baz) {} // for-in 循环左值 } const foo3 = function(bar) { for (bar of baz) {} // for-of 循环左值 }而下面的代码是正确的,因为它只是把参数读取后赋给一个新的局部变量,没有改动参数本身:
/*eslint no-param-reassign: "error"*/ const foo = function(bar) { const baz = bar; }源码视角:判定"写回"的核心逻辑
在 lib/rules/no-param-reassign.js 中,规则的create(context)通过sourceCode.getDeclaredVariables(node)获取函数声明变量,再借助reference.isWrite()判断引用是否为写入引用,从而报告assignmentToFunctionParam消息(消息模板为Assignment to function parameter '{{name}}'.)。值得注意的细节:
- 规则监听
FunctionDeclaration:exit、FunctionExpression:exit、ArrowFunctionExpression:exit三种节点,且必须使用:exit阶段,因为报告时需要依赖node.parent属性完成回溯判定。 checkReference中通过index === 0 || references[index - 1].identifier !== identifier去重,避免解构赋值中同一标识符产生多个可写引用时重复报告——解构赋值的默认值可能产生多个对同一标识符的写引用(见 lib/rules/no-param-reassign.js)。- 该规则基于
Reference/Variable的作用域分析机制工作,仅当variable.defs[0].type === "Parameter"时才检查,因此对全局变量的赋值(如someGlobal = 13)不会误报,这在测试用例中有明确覆盖。
该规则于 0.18.0 版本加入 eslint(见 docs/src/_data/rule_versions.json),并默认不推荐(recommended: false,见 docs/src/_data/rules_meta.json)。
Options:三个配置项详解
该规则接受一个对象选项,包含布尔属性"props",以及两个数组属性"ignorePropertyModificationsFor"与"ignorePropertyModificationsForRegex"。默认值为:
"props":默认false;"ignorePropertyModificationsFor":默认空数组[];"ignorePropertyModificationsForRegex":默认空数组[]。
当"props"为true时,规则会额外警告对参数属性的修改,除非该参数名出现在上述两个忽略数组中。
从 lib/rules/no-param-reassign.js 的schema定义可以看出,配置存在两套互斥结构(oneOf):当props为false时不允许携带其他属性(additionalProperties: false);当props为true时,才允许同时配置两个忽略数组,且数组元素为字符串、不允许重复(uniqueItems: true)。
props: false(默认行为)
在默认配置{ "props": false }下,修改参数对象的属性不会被报告,以下代码全部正确:
/*eslint no-param-reassign: ["error", { "props": false }]*/ const foo = function(bar) { bar.prop = "value"; } const foo1 = function(bar) { delete bar.aaa; } const foo2 = function(bar) { bar.aaa++; } const foo3 = function(bar) { for (bar.aaa in baz) {} } const foo4 = function(bar) { for (bar.aaa of baz) {} }这种模式非常适合"只读参数、可改写其内容"的场景(例如向传入的配置对象或缓存对象写入字段)。
props: true(同时检查属性修改)
当配置为{ "props": true }时,上述五类对参数属性的修改(赋值、delete、自增、for-in/for-of左值)全部变为错误:
/*eslint no-param-reassign: ["error", { "props": true }]*/ const foo = function(bar) { bar.prop = "value"; } const foo1 = function(bar) { delete bar.aaa; } const foo2 = function(bar) { bar.aaa++; } const foo3 = function(bar) { for (bar.aaa in baz) {} } const foo4 = function(bar) { for (bar.aaa of baz) {} }源码视角:isModifyingProp 如何识别属性修改
在props: true时,规则调用 lib/rules/no-param-reassign.js 中的isModifyingProp(reference),从参数标识符沿父节点向上回溯,直到遇到"停止节点"为止。stopNodePattern匹配以Statement、Declaration、Function(?:Expression)、Program结尾的节点类型,但对ForInStatement与ForOfStatement特殊放行,以便继续深入判定循环左值。
回溯过程中的关键分支(全部有对应测试用例佐证)包括:
AssignmentExpression:仅当参数标识符是赋值左侧(parent.left === node)时才判定为修改,例如bar.a = 0;UpdateExpression(如++bar.a):直接判定为修改;UnaryExpression:仅当操作符为delete(如delete bar.a)时判定为修改;ForInStatement/ForOfStatement:参数标识符位于parent.left时判定为修改(如for (bar.a in baz));- 明确排除的写法:
bar.get(0).a = 0(CallExpression中参数不在 callee 位置)、data[bar.a] = 0(MemberExpression中参数位于 property 位置)、({ [bar]: a } = value)(Property的 key)、(bar ? a : b).c = bar(ConditionalExpression的 test)——这些场景中参数并未被直接修改,不应误报。
测试文件 tests/lib/rules/no-param-reassign.js 的valid数组中大量此类"排除场景"用例,正是对这些边界行为的回归保障。
ignorePropertyModificationsFor:按参数名精确忽略
当props: true且某些参数确实允许修改其属性时,可以按精确名称列出例外。配置了"ignorePropertyModificationsFor": ["bar"]后,以下对bar属性的各类修改均为正确代码:
/*eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["bar"] }]*/ const foo = function(bar) { bar.prop = "value"; } const foo1 = function(bar) { delete bar.aaa; } const foo2 = function(bar) { bar.aaa++; } const foo3 = function(bar) { for (bar.aaa in baz) {} } const foo4 = function(bar) { for (bar.aaa of baz) {} }该数组同样支持并列多个参数名,例如测试中的ignorePropertyModificationsFor: ["a", "x"](见 tests/lib/rules/no-param-reassign.js)。在 lib/rules/no-param-reassign.js 的isIgnoredPropertyAssignment中,该数组通过Array.prototype.includes做全等匹配。
ignorePropertyModificationsForRegex:按正则模式忽略
当需要按命名模式忽略一批参数时,可使用正则数组。配置"ignorePropertyModificationsForRegex": ["^bar"]后,所有以bar开头的参数名(如barVar、barrito、bar_、barBaz)对属性的修改均为正确代码:
/*eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsForRegex": ["^bar"] }]*/ const foo = function(barVar) { barVar.prop = "value"; } const foo1 = function(barrito) { delete barrito.aaa; } const foo2 = function(bar_) { bar_.aaa++; } const foo3 = function(barBaz) { for (barBaz.aaa in baz) {} } const foo4 = function(barBaz) { for (barBaz.aaa of baz) {} }在源码实现中,该数组的每个字符串会在运行时通过new RegExp(ignored, "u")构造为正则对象(u标志表示 Unicode 模式,见 lib/rules/no-param-reassign.js),并对参数名执行.test()。因此这里的字符串是正则表达式而非普通通配符,例如测试中使用的"^a.*$"、"^(foo|bar)$"等模式(见 tests/lib/rules/no-param-reassign.js)。注意正则匹配是大小写敏感的(测试中"^B.*$"无法匹配bar,仍会报错)。
两个忽略数组可以同时使用:ignorePropertyModificationsForRegex负责正则匹配,ignorePropertyModificationsFor负责精确匹配,两者任一命中即放行。
常见边界场景速查
结合源码与测试,下表汇总了常见写法在props: true下的判定结果,便于快速查阅:
| 代码片段 | 判定结果 | 依据 |
|---|---|---|
bar = 13 | 报错(assignmentToFunctionParam) | 直接写引用 |
bar++/++bar/bar--/--bar | 报错 | UpdateExpression 写引用 |
bar += 13 | 报错 | 复合赋值 |
for (bar in baz)/for (bar of baz) | 报错 | 循环左值 |
({bar} = {})、[...bar] = obj、({...bar} = obj) | 报错 | 解构赋值写回 |
a &&= b、a \|\|= b、a ??= b | 报错 | 逻辑赋值(ES2021,见测试用例) |
bar.a = 0、delete bar.a、++bar.a、for (bar.a in {}) | 报错(assignmentToFunctionParamProp) | 属性修改 |
a.b &&= c、a[b] ??= c | 报错 | 逻辑赋值作用于属性 |
data[bar.a] = 0、bar.get(0).a = 0、({ [bar]: a } = value) | 不报错 | 参数非修改主体(源码显式排除) |
对全局变量赋值someGlobal = 13 | 不报错 | 仅检查 Parameter 定义变量 |
此外,function foo(a) { (function() { var a = 12; a++; })(); }不会被报告——内层函数中声明的是同名局部变量而非外层参数,作用域分析能正确区分。
配置示例:在 eslint.config.js 中使用
在 flat config 体系下,可以这样在 eslint.config.js 中按需启用该规则。例如:默认关闭属性检查,仅禁止参数重新赋值:
// eslint.config.js export default [ { rules: { "no-param-reassign": ["error", { props: false }] } } ];若希望函数不能以任何方式改动传入的参数对象(适合工具函数、纯函数为主的代码库):
// eslint.config.js export default [ { rules: { "no-param-reassign": ["error", { props: true }] } } ];对于"允许在参数对象上写入,但禁止替换参数引用"的常见模式(例如缓存对象、配置收集器),推荐组合使用:
// eslint.config.js export default [ { rules: { "no-param-reassign": [ "error", { props: true, ignorePropertyModificationsFor: ["cache", "options"], ignorePropertyModificationsForRegex: ["^ctx"] } ] } } ];该规则模块通过 lib/rules/index.js 的懒加载机制注册("no-param-reassign": () => require("./no-param-reassign")),因此可以直接以"no-param-reassign"作为规则名使用,无需额外安装插件。
When Not To Use It:何时应关闭此规则
如果你希望允许对函数参数赋值,可以放心地关闭该规则——它的目的只是帮助你规避非预期的参数修改,并非语言层面的强制约束。
需要特别说明的是与strict模式的关系:strict模式下的代码不会将arguments对象的索引与各个参数绑定同步。因此,在 ESM 模块或其他strict模式函数中,并不存在"改参数导致arguments对象被连带改动"的问题,也就无需靠该规则来防护arguments对象的意外变更。换句话说,该规则对arguments的保护价值主要体现在非strict模式的代码中;在纯strict模式项目里,是否启用它更多取决于你的团队对"参数是否可变"这一编程约定的偏好。
延伸阅读
- 规则完整文档:docs/src/rules/no-param-reassign.md
- 规则源码实现:lib/rules/no-param-reassign.js
- 规则测试用例:tests/lib/rules/no-param-reassign.js
- 规则元数据(类型、推荐状态、文档链接):docs/src/_data/rules_meta.json
- 规则引入版本记录:docs/src/_data/rule_versions.json
- 规则类型分类体系:conf/rule-type-list.json
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考