eslint-plugin-unicorn 规则解析:prefer-default-parameters 默认参数优先
eslint-plugin-unicorn 规则解析:prefer-default-parameters 默认参数优先
导读
prefer-default-parameters 是 eslint-plugin-unicorn 中一条用于消除函数参数"先接收、再重赋值"反模式的规则。它检测函数体内对参数进行的默认值重赋值(包括 = 配合 ||/??,以及 ||=/??= 逻辑赋值),并建议改用 ES6 默认参数语法。读完本文你将掌握:该规则的触发条件与例外场景、它与 null/undefined 语义差异的底层原理、修复器(suggestion)的工作方式,以及如何在 recommended/unopinionated 配置中启用它。
规则概览:把"重赋值"改写成"默认参数"
规则定义位于 rules/prefer-default-parameters.js,并在 rules/index.js 中以 prefer-default-parameters 名称导出。它属于 suggestion 类型的规则,通过 ESLint 的 editor suggestions 机制提供手动修复能力(hasSuggestions: true),不会自动改写代码,而是给出可一键应用的修复建议。
该规则在以下配置中默认启用:
- ✅
recommended(推荐配置) - ☑️
unopinionated(无观点配置)
核心规则:只报告字面量默认值的重赋值
匹配的三种模式
规则只报告对字面量值(Literal)的默认值重赋值,具体包括三种语法形态:
// 1. 赋值语句 + || 逻辑或(仅当 foo 为 falsy 时取 'bar')
function abc(foo) {
foo = foo || 'bar'; // ❌
}
// 2. 赋值语句 + ?? 空值合并(仅当 foo 为 null/undefined 时取 'bar')
function abc(foo) {
foo = foo ?? 'bar'; // ❌
}
// 3. 逻辑赋值运算符 ||= 与 ??=
function abc(foo) {
foo ||= 'bar'; // ❌
}
function abc(foo) {
foo ??= 'bar'; // ❌
}
从源码看,getDefaultAssignment 函数(rules/prefer-default-parameters.js)精确地刻画了这三种形态:
- 对
=赋值,要求右侧必须是LogicalExpression,且运算符为||或??,左操作数是标识符(即参数本身),右操作数是Literal; - 对
||=和??=,则直接要求右侧是Literal。
这意味着 foo = foo || bar()、foo = foo || {bar}、foo = foo && 'bar'、foo &&= 'bar' 这类"非纯字面量"或"非 ||/?? 运算符"的情况都不会被报告——测试用例中这些情形全部列为 valid(见 test/prefer-default-parameters.js)。
关键语义:字面量修复是安全的
为什么只处理字面量?因为 foo = foo || 123 中的 123 是无副作用的表达式,可以安全地挪进参数默认值。如果默认值是函数调用 bar(),移到参数默认值后,其求值时机会改变(每次调用前先求值,而不是在函数体内按需求值),可能导致行为变化,因此规则明确放行:
// ✅ 默认值包含调用,规则不报告
function abc(foo) {
foo = foo || bar();
}
测试中同样验证了 foo ||= bar() 不会被报告(test/prefer-default-parameters.js)。
另一个被支持的重构:新建变量承接默认值
除了直接重赋值参数,规则还支持把"用参数和 ||/?? 计算出的新变量"改写成参数默认值:
// ❌
function abc(foo) {
const bar = foo || 'bar';
}
// ✅
function abc(bar = 'bar') {}
对应测试见 test/prefer-default-parameters.js。此时要求该变量声明必须是函数体内唯一的声明(parent.declarations.length === 1),且该参数在默认赋值之前没有被其他代码引用。
为什么这是改进:|| 与默认参数在 falsy 值上的分歧
文档强调了一个容易踩坑的语义差异:foo = foo || 123 在 foo 为任何 falsy 值(0、''、false、NaN、null、undefined)时都会取 123,而默认参数只会在传入 undefined 时生效。
function abc(foo) {
foo = foo || 'bar';
}
abc(0); // foo 变为 'bar',0 被吞掉
abc(''); // foo 变为 'bar',空字符串被吞掉
function abc(foo = 'bar') {}
abc(0); // foo 仍为 0
abc(''); // foo 仍为 ''
因此这条规则实际在推动代码从"宽泛的 falsy 兜底"转向"精确的 undefined 兜底",避免函数在收到合法 falsy 入参时产生令人困惑的行为。
与 null 的关系:默认参数不处理 null
文档明确指出:如果你希望函数对 null 和其他 falsy 值采取与 undefined 相同的处理方式,应当禁用本规则。因为默认参数仅对 undefined 生效,传入 null 时参数仍为 null。项目官方态度是推荐 moving away from null(向 null 告别),认为更清晰的做法是用 undefined 表达"未传值"。
从源码实现看,?? 空值合并运算符是唯一能兼顾 null 与 undefined 的语法,但它与默认参数的语义仍然不完全等价——这也是为何修复建议只在字面量场景下安全。
规则的"红线":什么情况下拒绝报告
规则在 AST 层面做了大量保守检查,以下情况一律不报告(相应 valid 测试分布在 test/prefer-default-parameters.js):
1. 参数在默认赋值前已被引用
如果参数在赋值语句之前被使用过(如 console.log(foo)),改写后引用顺序会变化,规则放弃:
// ❌ 不报告
function abc(foo) {
console.log(foo);
foo = foo || 123;
}
源码中 hasExtraReferences(rules/prefer-default-parameters.js)负责检查:对于赋值语句,要求参数的第一个引用就是赋值本身;对于变量声明模式,要求参数在整个函数内只有这一处引用。
2. 赋值语句之前存在可能产生副作用的前置语句
如果默认赋值之前出现任何类调用表达式——函数调用 CallExpression、new 表达式、动态 import()、标签模板——规则放弃报告,因为默认参数会在这些副作用发生之前就完成求值,可能改变程序行为:
// ❌ 不报告(bar() 的副作用顺序会变)
function abc(foo) {
bar();
foo = foo || 123;
}
containsCallExpression(rules/prefer-default-parameters.js)递归遍历语句中的全部子节点,callLikeExpressionTypes 集合(rules/prefer-default-parameters.js)列出了这四类"调用类"节点;hasSideEffects(rules/prefer-default-parameters.js)再限定"在赋值语句之前出现的"才计入。测试专门覆盖了 new SideEffects()、import('side-effects')、sideEffects\template`` 等场景(test/prefer-default-parameters.js)。
3. 目标参数必须是最后一个参数
规则只处理函数的最后一个参数:
// ❌ 不报告(foo 不是最后一个参数)
function abc(foo, bar) {
foo = foo || 'bar';
}
源码中通过 params.at(-1) 取最后一个参数并校验类型与名称(rules/prefer-default-parameters.js),注释里明确这是为了避免与 default-param-last 规则冲突——ESLint 要求默认参数必须排在末尾,如果改写中间参数会违反该规则。Rest 参数(...foo)和已是 AssignmentPattern(foo = 'bar')的参数同样被排除。
4. 函数体包含 'use strict' 指令
带 'use strict' 的函数不会被报告(rules/prefer-default-parameters.js),测试见 test/prefer-default-parameters.js。
5. 参数名冲突与重复声明
如果函数内其他参数与默认值的目标变量同名(例如解构参数 {bar} 与要新建的 bar 变量冲突),或存在重复参数名(function abc(foo, foo)),规则放弃。hasParameterNameCollision 通过检查变量的定义节点是否为 Parameter 类型来判断(rules/prefer-default-parameters.js)。
6. 仅位于函数体顶层的语句
getDefaultParameterProblem 要求赋值语句的父节点必须是函数体本身(node.parent !== currentFunction.body 则跳过),因此嵌套在 if、for 等块级语句内的重赋值不会被报告:
// ❌ 不报告
function abc(foo) {
if (condition) {
foo ||= 'bar';
}
}
7. 只在"函数上下文"内工作
规则通过 functionStack 栈(rules/prefer-default-parameters.js)跟踪 FunctionDeclaration、FunctionExpression、ArrowFunctionExpression(functionTypes,见 rules/ast/function-types.js)三类函数。模块顶层 foo = foo || 'bar' 这种非函数语句自然不在检测范围(见 valid 测试 test/prefer-default-parameters.js)。
修复建议(Suggestion)是如何工作的
规则不提供自动修复,而是给出可手动应用的编辑器建议,消息 ID 为 preferDefaultParametersSuggest("Replace reassignment with default parameter.")。修复逻辑包含两个步骤(rules/prefer-default-parameters.js):
- 改写参数:把原参数名替换为
参数名 = 默认值(AssignmentPattern),必要时补上括号; - 删除原赋值语句:由
fixDefaultExpression(rules/prefer-default-parameters.js)处理,它会智能区分三种布局——赋值语句独占一行时整行删除、行尾带空格时连空格一并删除、否则仅删除节点本身。
括号补充:单参数箭头函数的特例
needsParentheses(rules/prefer-default-parameters.js)处理了单参数无括号箭头函数的括号问题:
// 原代码
const abc = foo => {
foo = foo || 'bar';
};
// 修复后必须补括号,否则语法错误
const abc = (foo = 'bar') => {
};
多参数函数不需要补括号,因为参数列表本身已带括号(测试见 test/prefer-default-parameters.js)。
带注释时不提供建议
如果赋值语句或参数内含有注释,修复建议会被 abort() 中止,仅报告错误而不给修复,避免破坏注释:
function abc(foo) {
foo ||= /* Keep comment. */ 'bar'; // 报告,但无 suggestions
}
对应测试见 test/prefer-default-parameters.js。
TypeScript 支持
规则支持 TypeScript 语法:修复时会保留参数的类型注解(typeAnnotation),把 foo?: string 的 foo ??= 'bar' 修复为 foo: string = 'bar'(去掉可选标记、补上默认值),并把 const bar: string = foo || 'bar' 重构为 bar: string = 'bar'。相关测试使用 parsers.typescript 解析器(test/prefer-default-parameters.js)。若参数类型注解处含注释,则不提供建议。
规则在 AST 层面的触发路径
规则的监听逻辑(rules/prefer-default-parameters.js)只挂载两个事件:
AssignmentExpression:仅当它是ExpressionStatement的完整表达式时处理(foo = foo || 123这种语句形态),并将运算符(=、||=、??=)传给getDefaultAssignment;VariableDeclarator:仅当它是单一声明的VariableDeclaration时处理(const bar = foo || 'bar'),运算符固定为=。
函数进入/退出时分别 push/pop 到 functionStack,确保检测始终落在正确的函数上下文中;最终通过 findVariable(来自 @eslint-community/eslint-utils)解析参数的引用关系,配合 hasExtraReferences、hasSideEffects、hasParameterNameCollision 三重校验后才产生报告。这也解释了为什么同一段重赋值代码,出现在不同函数或不同位置时,报告行为会截然不同。
实战建议与使用前提
- 启用方式:使用
recommended或unopinionated配置即可自动开启;也可在 flat config 中单独配置'unicorn/prefer-default-parameters': 'error'。 - 什么场景会得到报告:函数最后一个参数、函数体第一条语句、纯字面量默认值、无前置副作用/引用/命名冲突、非
'use strict'函数。 - 什么场景建议关闭:如果团队代码中
null是有意义的值(如数据库查询结果、外部 API 数据),且你希望null与undefined触发相同的兜底逻辑,应禁用本规则,因为默认参数无法覆盖null。 - 修复方式:手动应用编辑器的 suggestion,而不是依赖
--fix自动修改;规则本身是suggestion类型,与--fix自动修复机制相互独立。
延伸阅读
- 规则源码:rules/prefer-default-parameters.js
- 规则注册:rules/index.js
- 测试用例(覆盖全部 valid/invalid 与 suggestion 输出):test/prefer-default-parameters.js
- 相关 AST 工具:rules/ast/function-types.js、rules/ast/literal.js
- 相关规则概念:ESLint 官方
default-param-last(默认参数必须位于参数列表末尾的约束是本规则仅处理末位参数的原因之一)