eslint-plugin-unicorn 规则详解:consistent-function-scoping 把函数提升到最高作用域
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
导读
consistent-function-scoping是 eslint-plugin-unicorn 中一条在recommended配置里默认启用的规则,核心目标是把函数定义移动到"能捕获所需值的前提下尽可能高的作用域"(通常是最外层模块作用域),从而提升可读性并让 JS 引擎更容易做优化。本文以仓库中的规则快照报告为主线,结合规则文档、规则源码与测试用例,完整讲解该规则检测哪些函数、输出什么样的错误消息、如何配置,以及它刻意忽略哪些场景。
规则做什么:把函数放到它该待的最高层
一条简单的原则:函数定义应尽量靠近顶层作用域,前提是不能破坏它捕获的值。这带来两个实际收益:
- 可读性:函数定义集中在模块顶层,读者一眼能看到模块提供了哪些能力,而不是在函数嵌套里翻找;
- 性能:每次外层函数被调用,内层函数都会被重新创建,移出后函数对象只创建一次,同时引擎能更好地优化这类"热函数"。
快照报告中的 8 个 invalid 用例恰好把这条规则能捕获的"函数形态"完整地列了出来,是理解规则行为最直观的入口。
快照解读:规则检测哪 8 种"放错位置"的函数
快照报告 test/snapshots/consistent-function-scoping.js.md 由 AVA 在运行test.snapshot()(见 test/consistent-function-scoping.js)时生成,对应的invalid输入逐条展示了规则的错误输出格式。逐一来看:
1. 嵌套普通函数声明
function foo() { function bar() {} }输出:
> 2 | function bar() {} | ^^^^^^^^^^^^ Move function 'bar' to the outer scope.错误定位到bar的整个函数头,消息为Move function 'bar' to the outer scope.。
2. 嵌套 async 函数声明
function foo() { async function bar() {} }消息变为Move async function 'bar' to the outer scope.,函数"种类"被精确写进了错误消息。
3. 嵌套 generator 函数声明
function foo() { function * bar() {} }消息为Move generator function 'bar' to the outer scope.。
4. 嵌套 async generator 函数声明
function foo() { async function * bar() {} }消息为Move async generator function 'bar' to the outer scope.。function/async function/function */async function *四种声明形态全覆盖。
5. 用 const 声明的箭头函数
function foo() { const bar = () => {} }注意错误的下划线只标记在=>上(源码中通过getFunctionHeadLocation定位函数头),消息为Move arrow function 'bar' to the outer scope.。
6. return 语句中的嵌套箭头函数
const doFoo = () => bar => bar;消息为Move arrow function to the outer scope.—— 这个匿名箭头函数直接写在return里,没有名字,所以错误消息省略了函数名。源码中getParentNodeAfterNestedArrowFunctions专门处理"return 语句后跟着一串嵌套箭头函数"的情况,保证只报告真正能外移的内层函数。
7. const 声明的 async 箭头函数
function foo() { const bar = async () => {} }消息为Move async arrow function 'bar' to the outer scope.。
8. const 声明的函数表达式
function doFoo() { const doBar = function(bar) { return bar; }; }函数表达式同样被检测,错误标记落在function关键字上,消息为Move function 'doBar' to the outer scope.。注意与箭头函数不同,普通函数表达式在return语句中不会被标记(见源码 rules/consistent-function-scoping.js 的注释:普通函数表达式语义不同,外移可能改变this/arguments绑定,因此只有 return 中的箭头函数才会被跳过处理逻辑)。
从源码看,这些函数种类全部来自functionTypes(见 rules/ast/index.js 导出的 function-types.js),规则在 AST 遍历退出这些节点时触发检查(context.onExit(functionTypes, ...),见 rules/consistent-function-scoping.js)。
什么样才叫"可以外移":基于 scope 的引用分析
规则不会盲目报告所有嵌套函数——只有当函数没有捕获外层作用域的任何东西时才认为它可以提升。这依赖于 ESLint 的scopeManager做引用分析,核心实现在 checkReferences:
- 收集函数作用域内所有引用(
getReferences会连同所有子作用域的引用一起展平,见 get-references.js); - 只要任一引用解析到的定义/作用域落在"父作用域集合"(
parentScopes)里,就说明函数捕获了外层变量,不能外移; - 对递归函数名(
definition.type === 'FunctionName'且名字与函数自身一致)做特殊跳过,避免把递归误判为"捕获外部变量"; - 对相邻函数声明做额外检查(
hitIdentifier),处理兄弟函数之间的引用关系。
对应的 valid 测试用例能帮你快速建立直觉(见 test/consistent-function-scoping.js):
// ✅ 捕获了参数 foo,不能外移 function doFoo(foo) { function doBar(bar) { return foo + bar; } return foo; } // ✅ 捕获了块级变量 foo,不能外移 function doFoo() { const foo = 'foo'; function doBar(bar) { return foo + bar; } return foo; }而完全不捕获任何外层值的函数,即使只是"躺"在函数里从未使用,也会被报告(见快照用例 1-8 以及测试中的function doFoo() { function doBar() {} }等 invalid 用例)。
嵌套箭头函数链与循环体:两个容易被误报的场景
快照用例 6 只是冰山一角,源码里还有两处专门处理复杂嵌套的逻辑:
返回语句中的箭头函数链:
return next => action => {...}这种柯里化写法在 Redux middleware 中很常见。规则通过getParentNodeAfterNestedArrowFunctions/skipArrowFunctionChain正确处理"从 ReturnStatement 出发穿过一串箭头函数"的情况,只标记真正不再捕获外部值的层。测试中的export function middleware(store) { return next => action => {...} }(捕获store)是 valid,而function middleware() { return next => action => {...} }(不捕获任何外部值)会报两个错误(见 test/consistent-function-scoping.js)。循环体内的函数:
for/while/do...while循环体内的块级作用域与循环语句作用域是分开的。getRelevantParentScopes(rules/consistent-function-scoping.js)会把循环语句自身的作用域也加入parentScopes一并比较,从而正确判断"循环内定义、只捕获循环变量的函数"不能外移(因为外移后循环变量就没了)。测试中用了一整组 valid 用例覆盖for...of/for...in/for(;;)/while/do...while及嵌套块的各种组合(test/consistent-function-scoping.js),对应的const match = () => true;不捕获循环变量、可以外移的用例则是 invalid(test/consistent-function-scoping.js)。
配置选项:checkArrowFunctions
规则的 JSON Schema 定义在 rules/consistent-function-scoping.js,只有一个选项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
checkArrowFunctions | boolean | true | 是否检查箭头函数 |
配置示例(关闭对箭头函数的检查,只检查普通函数声明/表达式):
{ "rules": { "unicorn/consistent-function-scoping": ["error", {"checkArrowFunctions": false}] } }关闭后,function outer() { const inner = () => {} }不再报错,但function outer() { function inner() {} }仍然会被报告——测试用例专门验证了这一行为(test/consistent-function-scoping.js 与 test/consistent-function-scoping.js)。defaultOptions中的默认值保证不传选项时行为一致(rules/consistent-function-scoping.js)。
规则刻意忽略的四类场景
文档 Limitations 一节和源码共同确认了以下"误报豁免区",了解它们能避免你误以为规则有 bug:
1. 函数内多余的花括号代码块
function doFoo(foo) { { function doBar(bar) { return bar; } } return foo; }规则不检测也不移除函数内部多余代码块里的函数。注意这只针对"额外嵌套的块":直接嵌套在函数体里的函数仍会被检查(对应 invalid 用例,见 test/consistent-function-scoping.js)。
2. IIFE(立即调用函数表达式)
(function () { function doFoo(bar) { return bar; } })();IIFE 内的函数被整体豁免,因为 IIFE 本身就是一种作用域隔离手段。源码通过isIife判断节点是否为"父节点是 CallExpression 且自身就是 callee"的函数表达式/箭头函数(rules/consistent-function-scoping.js)。测试覆盖了(function(){...})()、(function(){...}())、!function(){...}()、(() => {...})()、async IIFE、async generator IIFE 以及 IIFE 与非 IIFE 混在参数列表里的复杂用例(test/consistent-function-scoping.js、test/consistent-function-scoping.js)。注意 IIFE内部再嵌套的函数不受豁免,仍会正常报告。
3. React 内置 Hooks 的回调
useEffect(() => { async function getItems() {} getItems(); }, [])useState/useEffect/useContext/useReducer/useCallback/useMemo/useRef等 React 内置 Hooks(含React.xxx前缀写法,完整列表见 rules/consistent-function-scoping.js)的回调作用域被豁免,避免把 Hooks 惯用法误报为"应该外移"。同时NotReact.useEffect这类非 React 调用不会获得豁免(对应 invalid 用例,见 test/consistent-function-scoping.js)。
4. jest.mock 工厂函数
jest.mock('module', () => { function createMock() { return 'mock'; } return createMock; });Jest 限制 mock 工厂不能引用作用域外的变量,因此工厂内的函数即使"看起来可以外移"也必须留在原地。源码通过isInsideJestMockFactory向上遍历 AST,匹配jest.mock/jest.doMock的第二个参数(rules/consistent-function-scoping.js)。测试还验证了jest.doMock("module", () => () => "mock")这种工厂内直接返回箭头函数的内层函数仍会被报告(test/consistent-function-scoping.js)。
this 与 arguments:箭头函数的词法敏感检查
箭头函数没有自己的this/arguments,如果箭头函数体内引用了词法this(或参数默认值引用了this),把它外移会改变this的绑定语义。规则通过isNodeContainsLexicalThis(配合 AST visitorKeys 做词法级检查,而非依赖 parser 的scope.thisFound元数据)对这类箭头函数直接跳过(rules/consistent-function-scoping.js)。对应测试用例:
// ✅ 捕获 this 的箭头函数,不外移 function doFoo(Foo) { const doBar = () => this; return doBar(); }; // ✅ 多级嵌套箭头函数链中的 this 同样被识别 function doFoo(Foo) { const doBar = () => () => () => this; return doBar(); }; // ✅ 参数默认值引用 this 也会被识别 function doFoo(Foo) { const doBar = (value = this) => value; return doBar(); };见 test/consistent-function-scoping.js。而引用arguments的普通函数声明会被正常报告(它有自己的arguments对象,外移不影响语义),测试中function doBar() { return arguments; }是 invalid(test/consistent-function-scoping.js)。
实际修复示例:把函数从嵌套中解放出来
规则文档给出了正反对照的修复模式。最常见的"坏味道"是函数不捕获任何外层值却被定义在内部:
// ❌ 不捕获任何外部值,却被包在 doFoo 里 export function doFoo(foo) { function doBar(bar) { return bar === 'bar'; } return doBar; } // ✅ 提到模块顶层 function doBar(bar) { return bar === 'bar'; } export function doFoo(foo) { return doBar; }箭头函数同理:
// ❌ export function someAction() { return dispatch => dispatch({type: 'SOME_TYPE'}); } // ✅ const handleDispatch = dispatch => dispatch({type: 'SOME_TYPE'}); export function someAction() { return handleDispatch; }而确实捕获了外层值的函数(比如捕获参数foo、使用foo.doBar的场景)即使嵌套也保持原样,规则不会误报(对应文档末尾的 ✅ 示例)。
在项目中使用:启用、验证与快照测试
- 启用:该规则属于
recommended配置(meta.docs.recommended: true,见 rules/consistent-function-scoping.js),并在 rules/index.js 中注册导出。使用recommended配置即默认开启;unopinionated配置中该规则被禁用。 - 手动验证:仓库用 AVA 运行规则测试,快照用例通过
test.snapshot({invalid: [...]})生成(test/consistent-function-scoping.js),快照本体保存在test/snapshots/consistent-function-scoping.js.snap,而其人类可读版即本文引用的 test/snapshots/consistent-function-scoping.js.md。如果你修改了规则行为,需要运行对应的快照更新命令让快照同步。 - 类型安全:规则还附带 TypeScript 测试套件(
test.typescript(...)),覆盖泛型箭头函数链、FinalizationRegistry回调中的this、this: unknown显式标注等 TS 场景(test/consistent-function-scoping.js),确认规则对 TS 语法同样安全。
小结
consistent-function-scoping通过 ESLint 作用域分析,把"不捕获外层值却被层层嵌套"的函数精准识别出来,并在错误消息中区分function、async function、generator、箭头函数等形态(对应快照报告的 8 个代表性用例),同时为 IIFE、React Hooks、jest.mock 工厂、词法this等合理场景设置了豁免。理解它背后的parentScopes引用分析、循环体作用域合并和嵌套箭头函数链处理,你就能在启用该规则时准确预判报告结果,写出"函数就该待在它最高该待的位置"的代码。
【免费下载链接】eslint-plugin-unicornMore than 300 powerful ESLint rules项目地址: https://gitcode.com/GitHub_Trending/es/eslint-plugin-unicorn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考