eslint-plugin-unicorn 规则解析:prefer-default-parameters 默认参数优先

原创2026-09-18 18:06:11255 阅读
文章标签:Lint代码质量

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):

  1. 改写参数:把原参数名替换为 参数名 = 默认值(AssignmentPattern),必要时补上括号;
  2. 删除原赋值语句:由 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 自动修复机制相互独立。

延伸阅读

登录后查看全文
eslint-plugin-unicorn