ESLint no-return-assign 规则详解:杜绝 return 语句中的赋值歧义
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
本指南以 ESLint 内置规则no-return-assign为核心,讲解为什么return语句中的赋值表达式是代码异味,以及该规则如何通过except-parens与always两种模式帮你消除这种歧义。读完本文,你将掌握该规则的完整配置方法、父子选项的差异、与箭头函数相关的特殊行为,以及底层源码的实现原理和测试验证方式。
背景:return中的赋值为何危险
JavaScript 的一大特点是赋值几乎可以出现在任何表达式位置。正因如此,一个多写(或少写)的等号很容易在真正想比较时却执行了赋值,这在return语句中尤为常见。例如:
function doSomething() { return foo = bar + 2; }此处return的意图难以判断:可能是想返回bar + 2的计算结果,但为何又要把它赋给foo?也可能是本意是使用比较运算符(如==),这段代码其实是个错误。
由于这种歧义的存在,社区公认的最佳实践是:不要在return语句中使用赋值表达式。no-return-assign规则正是为了实现这一最佳实践而存在的。该规则从 ESLint 早期版本(本仓库数据 docs/src/_data/rule_versions.json 显示其引入版本为0.0.9)一直保留至今,属于suggestion(建议类)规则,默认不开启(recommended: false,见 docs/src/_data/rules.json 与 lib/rules/no-return-assign.js)。
规则详情:捕获范围与上报方式
规则的目标是消除return语句中的赋值,只要在return的返回值表达式(或箭头函数的表达式体)中发现赋值,就会给出警告。
需要注意的是,规则关注的是返回值表达式本身中的赋值。若赋值出现在return之后嵌套的函数体内,则不会被这条规则误伤。例如:
function x() { return function y() { result = a * b }; // 正确:赋值在嵌套函数体内,不属于外层 return 的返回值 }这条规则对应的报告消息有两条(定义于 lib/rules/no-return-assign.js 的meta.messages):
| messageId | 消息文本 | 触发场景 |
|---|---|---|
returnAssignment | Return statement should not contain assignment. | 普通函数/块体内的return返回了赋值 |
arrowAssignment | Arrow function should not return assignment. | 箭头函数以表达式体直接返回赋值 |
选项配置
规则接受一个字符串选项,取值必须为以下两者之一(对应 lib/rules/no-return-assign.js 中schema的enum: ["except-parens", "always"]):
| 选项值 | 含义 | 默认值 |
|---|---|---|
except-parens | 禁止赋值,除非赋值被括号包裹 | ✅ 默认 |
always | 禁止一切赋值(括号包裹也不行) | ❌ |
由于defaultOptions: ["except-parens"](见源码 lib/rules/no-return-assign.js 与 docs/src/_data/rules_meta.json),在eslint.config.js(flat config)中可以这样显式配置:
export default [ { files: ["**/*.js"], rules: { "no-return-assign": "error", // 使用默认 "except-parens" "no-return-assign": ["error", "except-parens"], // 显式写清默认值 "no-return-assign": ["error", "always"], // 最严格模式 }, }, ];选项一:except-parens(默认)
这是默认选项。它禁止在return中使用赋值,除非整个赋值表达式被括号包围。括号在这里充当"我是有意的"显式声明,告诉读者:没错,我就是要返回一个赋值表达式的结果。
默认选项下的错误示例
/*eslint no-return-assign: "error"*/ function doSomething() { return foo = bar + 2; } function doSomethingElse() { return foo += 2; } const foo = (a, b) => a = b const bar = (a, b, c) => (a = b, c == b) function doSomethingMore() { return foo = bar && foo > 0; }以上全部违规:前两者是普通函数return直接返回赋值;const foo = (a, b) => a = b是箭头函数表达式体直接返回赋值;const bar = (a, b, c) => (a = b, c == b)中a = b只是被逗号表达式包裹,整个表达式并未被括号整体包围,因此仍然违规;最后一个return foo = bar && foo > 0;中的赋值出现在逻辑表达式中,也未整体加括号,违规。
默认选项下的正确示例
/*eslint no-return-assign: "error"*/ function doSomething() { return foo == bar + 2; } function doSomethingElse() { return foo === bar + 2; } function doSomethingMore() { return (foo = bar + 2); } const foo = (a, b) => (a = b) const bar = (a, b, c) => ((a = b), c == b) function doAnotherThing() { return (foo = bar) && foo > 0; }正确示例的要点:
==/===是比较运算而非赋值,永远合法;return (foo = bar + 2);整个赋值表达式被括号包围,合法;- 箭头函数
const foo = (a, b) => (a = b)的表达式体整体带括号,合法; ((a = b), c == b)中赋值与整个逗号表达式都被括号包围,合法;(foo = bar) && foo > 0中只有赋值部分被括号包围,逻辑与表达式本身不在括号内——但规则只要求赋值表达式自身被括号包裹,因此合法(这点与上面(a = b, c == b)违规形成鲜明对比)。
选项二:always
always选项禁止return语句中一切赋值,括号包裹也不能豁免。所有赋值都被视为问题。
always 选项下的错误示例
/*eslint no-return-assign: ["error", "always"]*/ function doSomething() { return foo = bar + 2; } function doSomethingElse() { return foo += 2; } function doSomethingMore() { return (foo = bar + 2); }注意最后一条:在except-parens下合法的return (foo = bar + 2);,在always下同样违规。
always 选项下的正确示例
/*eslint no-return-assign: ["error", "always"]*/ function doSomething() { return foo == bar + 2; } function doSomethingElse() { return foo === bar + 2; }在always模式下,唯一安全的是比较运算。若确有"先赋值再返回该值"的需求,应把赋值拆到return之前的语句中:
function doSomething() { const result = bar + 2; return result; }与相关规则的边界
no-return-assign只关心return/ 箭头函数表达式体中的赋值,若项目还需要管控其他位置的赋值歧义,可在本仓库 lib/rules 中寻找配套规则:
- no-cond-assign:禁止条件判断(如
if、while)中的赋值; - no-assign-ternary:禁止三元表达式分支中的赋值(该规则在本仓库中为实验性规则,需开启实验性 flag)。
三者互补,可以覆盖赋值表达式在常见语法位置的全部隐患。
何时不使用此规则
如果你希望允许在return语句中使用赋值运算符(例如团队代码风格依赖return x = y这种紧凑写法,或某些库的回调模式需要返回赋值结果),可以放心关闭此规则:
export default [ { rules: { "no-return-assign": "off", }, }, ];关闭后规则不产生任何检查,也不会影响其他规则工作。
源码剖析:规则如何判定"括号包裹"与"祖先链"
阅读 lib/rules/no-return-assign.js 的create(context)实现,可以完整还原规则的判定逻辑。
第一步:括号豁免检查
规则监听每一个AssignmentExpression节点。若选项不是except-parens(即always),直接跳过括号判断;否则调用工具函数astUtils.isParenthesised(sourceCode, node):
const always = context.options[0] !== "except-parens"; ... AssignmentExpression(node) { if (!always && astUtils.isParenthesised(sourceCode, node)) { return; } ... }isParenthesised的实现位于 lib/rules/utils/ast-utils.js,它通过比较赋值表达式节点前后 token 是否为(/),并校验括号的 range 是否紧贴节点边界,从而精确判定该节点是否被括号"整体包裹"——这正是前面"只有赋值本身被括号包围才放行"的机制来源。这也是为什么(result) = (a * b)这种只在标识符两侧加括号的写法仍然违规(括号没有包住整个赋值节点)。
第二步:沿祖先链寻找"哨兵节点"
跳过括号检查后,规则用一个SENTINEL_TYPE正则沿祖先链向上查找:
const SENTINEL_TYPE = /^(?:[a-zA-Z]+?Statement|ArrowFunctionExpression|FunctionExpression|ClassExpression)$/u;从当前赋值节点出发,逐个向上遍历父节点,直到遇到第一个"哨兵"——即任意*Statement(语句节点,如ExpressionStatement、ReturnStatement)或函数/类表达式。这样做的目的是定位赋值所属的最近语句边界,避免跨越函数边界误报(比如把内层函数体内的赋值算到外层return头上)。
第三步:分场景上报
- 若祖先链尽头是
ReturnStatement,则报returnAssignment; - 若尽头是
ArrowFunctionExpression且该赋值节点正是箭头函数表达式体(parent.body === currentChild),则报arrowAssignment; - 若尽头是
FunctionExpression/ClassExpression或普通语句,说明赋值不在return路径上,不报告。
这条实现路径解释了为何return foo = () => a(返回一个箭头函数的同时对foo赋值)报returnAssignment,而const foo = (a) => (b) => a = b(柯里化箭头函数的最内层表达式体直接返回赋值)报arrowAssignment。
测试验证:规则行为的完整清单
规则行为由 tests/lib/rules/no-return-assign.js 通过RuleTester全面覆盖,测试用例与官方文档示例一一对应,并补充了大量边界场景:
valid(不报错)关键用例包括:
function x() { return (result = a * b); }(默认与except-parens下合法);() => { return (result = a * b); }与() => (result = a * b)(块体加括号与表达式体加括号均合法);const foo = (a,b,c) => ((a = b), c)(整个逗号表达式加括号合法);function x() { return function y() { result = a * b }; }(嵌套函数体中的赋值不受外层return牵连);const foo = (a) => (b) => (a = b)(多层箭头函数逐层加括号合法);module.exports = {'a': 1};(顶层赋值不涉及return,合法)。
invalid(报错)关键用例包括:
function x() { return result = a * b; }→returnAssignment;function x() { return (result) = (a * b); };→returnAssignment(括号未包住整个赋值节点,默认模式也报错);() => result = a * b→arrowAssignment;return result || (result = a * b);(always下逻辑表达式中的赋值也报错);return foo = function(){ return (bar = bar1) }→returnAssignment;return () => a = () => b→arrowAssignment(返回的箭头函数表达式体含赋值);const foo = (a) => (b) => a = b→arrowAssignment(多层箭头函数最内层未加括号)。
这些用例同时验证了规则对ecmaVersion: 6及以上语法(箭头函数)的支持,以及languageOptions配置在RuleTester中的正确用法。
小结
no-return-assign是一条简单但高频实用的建议类规则:默认except-parens模式下,它要求任何出现在return/ 箭头函数表达式体中的赋值都必须用括号显式声明意图;always模式则彻底禁止此类写法,倒逼开发者把赋值拆分为独立语句。结合本仓库中的源码实现(lib/rules/no-return-assign.js)、工具函数(lib/rules/utils/ast-utils.js)与完整测试(tests/lib/rules/no-return-assign.js),你可以精确预判该规则在任意赋值形态下的判定结果,并据此决定是开启、加严还是关闭它。
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考