十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ESLint no-use-before-define 规则深度解析:如何根除 JavaScript/TypeScript 中的“先使用后声明”

ESLint no-use-before-define 规则深度解析:如何根除 JavaScript/TypeScript 中的“先使用后声明” ESLint no-use-before-define 规则深度解析如何根除 JavaScript/TypeScript 中的“先使用后声明”【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslintno-use-before-define是 ESLint 内置的problem型规则用于在 JavaScript 代码中发现“标识符在被声明之前就被引用”的隐患。本文以官方规则文档 docs/src/rules/no-use-before-define.md 为主体结合规则实现源码 lib/rules/no-use-before-define.js 与完整测试套件 tests/lib/rules/no-use-before-define.js系统讲解该规则的检查逻辑、全部配置项functions、classes、variables、allowNamedExports以及 TypeScript 专属的enums、typedefs、ignoreTypeReferences与nofunc简写并深入源码剖析其作用域分析、暂时性死区判断与类静态初始化器等底层实现原理。读完本文你将能精确配置该规则以适配团队编码风格并理解它为什么能覆盖var、let、const、函数、类、class 静态块、ES Module 具名导出与 TypeScript 类型声明等全部场景。规则背景提升Hoisting与暂时性死区TDZ在 ES6 之前JavaScript 中的变量声明与函数声明会被提升到其所在作用域的顶部因此在代码中“先使用、后声明”是语法上合法的行为。例如alert(a); // 可以运行a 为 undefined var a 10;这种写法虽然能运行却容易让人困惑——代码阅读者很难判断a此刻到底是undefined、全局变量还是尚未初始化于是许多团队约定“先声明、后使用”。ES6 引入块级绑定let、const之后情况变得更加严格在声明语句执行之前访问该绑定会触发暂时性死区Temporal Dead Zone, TDZ直接抛出ReferenceError。例如{ alert(c); // ReferenceError: Cannot access c before initialization let c 1; }正是为了把这类“运行期才会暴露”的问题提前到静态检查阶段ESLint 提供了no-use-before-define规则。从 lib/rules/no-use-before-define.js 的元数据可以看到它被归类为type: problem说明这是一类会导致实际运行问题的代码并同时支持 JavaScript 与 TypeScript 两种方言dialects: [JavaScript, TypeScript]但在官方eslint:recommended配置中默认不开启recommended: false需要使用者显式启用。规则详情它到底检查什么规则的核心行为一句话即可概括当它发现某个标识符的引用发生在该标识符声明之前时报告一个错误。报告的消息文本定义在源码的messages字段中messages: { usedBeforeDefined: {{name}} was used before it was defined., }下面先看规则在默认配置即[error, {}]等价于全部选项取默认值下判定为错误的代码/*eslint no-use-before-define: error*/ alert(a); var a 10; f(); function f() {} function g() { return b; } var b 1; { alert(c); let c 1; } { class C extends C {} } { class C { static x foo; [C.x]() {} } } { const C class { static x C; } } { const C class { static { C.x foo; } } } export { foo }; const foo 1;这些错误案例覆盖了规则关心的全部绑定形态var变量alert(a)在var a 10之前、函数声明f()在function f() {}之前、跨函数作用域的变量引用g()内部的return b、块级letTDZ、类的自引用与继承class C extends C {}、class 静态字段与静态块中对类名自身的引用static x C、static { C.x foo }以及 ES Module 的具名导出export { foo }在const foo 1之前。而下面的代码则全部合法注意观察它们与错误版本的结构差异/*eslint no-use-before-define: error*/ var a; a 10; alert(a); function f() {} f(1); var b 1; function g() { return b; } { let c; c; } { class C { static x C; } } { const C class C { static x C; } } { const C class { x C; } } { const C class C { static { C.x foo; } } } const foo 1; export { foo };正确案例传达了几个关键语义赋值与引用的区别var a; a 10; alert(a);中先声明后赋值再引用没有问题class C { static x C; }之所以合法是因为类绑定在静态初始化器执行之前就已经被初始化源码注释明确说明 “Class binding is initialized before running static initializers”。命名类表达式与匿名类的区别const C class C { static x C; }中static x C引用的是命名类表达式内部的类名绑定它在初始化时已就绪而const C class { static x C; }引用的是外层const C在该赋值语句完成前尚未初始化因此报错。实例字段与静态字段的区别const C class { x C; }合法因为实例字段初始化器是在实例化时才执行的“隐式函数”属于独立的执行上下文而静态字段在类定义求值阶段就会运行。export与import的对称性const foo 1; export { foo };先声明再导出合法反之则报错。配置选项总览规则的完整配置形式如下{ no-use-before-define: [error, { functions: true, classes: true, variables: true, allowNamedExports: false, enums: true, typedefs: true, ignoreTypeReferences: true }] }各选项含义与默认值选项类型默认值作用functionsbooleantrue是否检查函数声明被提前引用classesbooleantrue是否检查上层作用域中的类声明被提前引用variablesbooleantrue是否检查上层作用域中的变量声明被提前引用allowNamedExportsbooleanfalse是否始终放行export {};中的引用enumsbooleantrueTypeScript是否检查enum被提前引用typedefsbooleantrueTypeScript是否检查type别名 /interface被提前引用ignoreTypeReferencesbooleantrueTypeScript是否忽略类型注解、类型断言等纯类型位置上的引用此外规则还接受字符串选项nofunc它等价于显式展开为{ functions: false, classes: true, variables: true, allowNamedExports: false, enums: true, typedefs: true, ignoreTypeReferences: true }从源码看这些配置项的合法性由 lib/rules/no-use-before-define.js 中的schema校验要么是字符串枚举nofunc要么是一个对象其可接受属性仅限上述七个布尔选项additionalProperties: false传入未知键会直接报配置错误。默认值则记录在defaultOptions字段lib/rules/no-use-before-define.js并在运行期由parseOptions函数解析lib/rules/no-use-before-define.js对象直接使用nofunc字符串把functions置为false其余全部取默认值未传任何选项时则全部取默认值。这些元数据同样维护在 docs/src/_data/rules_meta.json 中供文档站点自动渲染。functionsfunctions决定规则是否检查函数声明被提前引用为true时对函数声明之前的每一次引用都会告警为false时忽略这类引用。因为函数声明会被提升hoisted关闭此选项在运行期通常是安全的。但需要注意一些惯用法例如相互递归function even(n){ return n 0 || odd(n-1); } function odd(n){ return n ! 0 even(n-1); }依赖函数提升此时就必须把functions设为false。{ functions: false }下的正确示例/*eslint no-use-before-define: [error, { functions: false }]*/ f(); function f() {}需要特别强调的是该选项只放行函数声明。对于函数表达式与箭头函数它们本质是变量绑定不存在提升请使用下方的variables选项来控制——例如f(); const f () {};这类写法仍会受variables管辖。classesclasses决定规则是否检查上层作用域中的类声明被提前引用为true时对类声明之前的每一次引用如new A()都会告警为false时忽略“声明位于外层函数作用域”的引用。类声明不会提升关闭此选项可能存在运行期风险ReferenceError官方文档也提示“关闭它可能是危险的”因此建议保持默认开启。{ classes: false }下的错误示例注意即便是false以下写法依然会被报告因为它们处于同一执行上下文中/*eslint no-use-before-define: [error, { classes: false }]*/ new A(); class A { } { class C extends C {} } { class C extends D {} class D {} } { class C { static x foo; [C.x]() {} } } { class C { static { new D(); } } class D {} }正确示例——引用发生在独立的函数执行上下文中且类的声明在外层作用域/*eslint no-use-before-define: [error, { classes: false }]*/ function foo() { return new A(); } class A { }这里的判断逻辑值得展开即使classes: false同作用域或同一执行上下文内的“类先使用后声明”依然会被报告因为new A(); class A {}直接违反 TDZ而foo函数体中的new A()只有在该函数被调用时才执行此时类早已定义完毕因此被放行。这正是源码中isFromSeparateExecutionContext辅助函数lib/rules/no-use-before-define.js的核心职责——它沿着作用域链向上比较“变量作用域”variableScope代表执行上下文是否一致只有引用确实来自独立的执行上下文时才允许关闭对应选项。variablesvariables决定规则是否检查上层作用域中的变量声明被提前引用为true时对变量声明之前的每一次引用都会告警为false时忽略“声明位于上层作用域”的引用但如果引用与声明处于同一作用域依然会报告。{ variables: false }下的错误示例/*eslint no-use-before-define: [error, { variables: false }]*/ console.log(foo); var foo 1; f(); const f () {}; g(); const g function() {}; { const C class { static x C; } } { const C class { static x foo; } const foo 1; } { class C { static { this.x foo; } } const foo 1; }正确示例——引用与声明分处不同执行上下文/*eslint no-use-before-define: [error, { variables: false }]*/ function baz() { console.log(foo); } var foo 1; const a () f(); function b() { return f(); } const c function() { return f(); } const f () {}; const e function() { return g(); } const g function() {} { const C class { x foo; } const foo 1; }对照两组示例可以提炼出规律console.log(foo); var foo 1;同作用域必报function baz() { console.log(foo); } var foo 1;引用发生在函数体内独立执行上下文variables: false时放行f(); const f () {};中f是const绑定的函数表达式属于变量而非函数声明仍受variables约束故报错类实例字段x foo;是隐式函数实例化时才执行属于独立执行上下文可放行而静态字段static x foo;与静态块static { this.x foo; }在类定义求值阶段即运行属于父执行上下文即使variables: false也照常报错。上述行为在源码中由两个辅助函数精确建模isClassStaticInitializerScopelib/rules/no-use-before-define.js识别class-static-block与静态字段初始化器class-field-initializer且对应PropertyDefinition.static true这两类特殊作用域isFromSeparateExecutionContext则在向上寻找变量作用域的过程中把“类静态初始化器”当作父执行上下文的一部分因为它们在类定义求值期间自动运行其余跨越函数边界的情况一律判定为独立执行上下文。allowNamedExportsallowNamedExports若设为true规则将始终放行export {};声明中的引用。由于具名导出语句export { a, b }只是声明“这些名字将被导出”并不会在此时读取它们的值因此即使变量在后面才声明引用也是安全的模块求值完成时它们必然已初始化。{ allowNamedExports: true }下的正确示例/*eslint no-use-before-define: [error, { allowNamedExports: true }]*/ export { a, b, f, C }; const a 1; let b; function f () {} class C {}错误示例——放行仅限具名导出export default及普通引用依旧被检查/*eslint no-use-before-define: [error, { allowNamedExports: true }]*/ export default a; const a 1; const b c; export const c 1; export function foo() { return d; } const d 1;从实现上看这一逻辑位于shouldCheck函数中lib/rules/no-use-before-define.js当allowNamedExports为true且标识符的父节点是ExportSpecifier且标识符就是该导出说明符的local端时直接返回false跳过检查export default a的标识符父节点是ExportDefaultDeclaration而非ExportSpecifier所以不受此豁免。TypeScript 扩展enums / typedefs / ignoreTypeReferences规则在默认配置下同样覆盖 TypeScript 的enum、type别名与interface并额外提供三个选项细化控制。启用 TypeScript 检查时需要把对应文件交给启用了 TypeScript 解析器的 ESLint 实例处理例如在languageOptions.parser中配置typescript-eslint/parser。enumsTypeScript onlyenums为true默认时规则会检查enum被提前引用的情况/*eslint no-use-before-define: [error, { enums: true }]*/ const x Foo.FOO; enum Foo { FOO, }先定义后使用的正确写法/*eslint no-use-before-define: [error, { enums: true }]*/ enum Foo { FOO, } const x Foo.FOO;在源码中enum绑定由eslint-scope标记为TSEnumName定义类型shouldCheck通过!options.enums definitionType TSEnumName决定是否豁免lib/rules/no-use-before-define.js。typedefsTypeScript onlytypedefs为true默认时规则会检查type别名与interface被提前引用的情况为false时允许先使用后定义。类型位置的引用受ignoreTypeReferences的联合影响因此下面的示例显式把ignoreTypeReferences设为false以便观察纯类型引用/*eslint no-use-before-define: [error, { typedefs: true, ignoreTypeReferences: false }]*/ let myVar: StringOrNumber; type StringOrNumber string | number; const x: Foo {}; interface Foo {}先定义后使用的正确写法/*eslint no-use-before-define: [error, { typedefs: true, ignoreTypeReferences: false }]*/ type StringOrNumber string | number; let myVar: StringOrNumber; interface Foo {} const x: Foo {};type别名与interface的定义类型在 scope 分析中被归为TypeshouldCheck中的对应分支为!options.typedefs definitionType Typelib/rules/no-use-before-define.js。ignoreTypeReferencesTypeScript onlyignoreTypeReferences为true默认时规则会忽略所有纯类型位置上的引用例如类型注解、类型断言as T、satisfies表达式、typeof类型查询等场景。将其设为false后类型引用同样会被纳入检查/*eslint no-use-before-define: [error, { ignoreTypeReferences: false }]*/ let var1: StringOrNumber; type StringOrNumber string | number; let var2: Enum; enum Enum {}先定义后使用的正确写法/*eslint no-use-before-define: [error, { ignoreTypeReferences: false }]*/ type StringOrNumber string | number; let myVar: StringOrNumber; enum Enum {} let var2: Enum;当ignoreTypeReferences: false且typedefs: false时type/interface的前置引用被放行而enum依然受enums选项约束/*eslint no-use-before-define: [error, { ignoreTypeReferences: false, typedefs: false, }]*/ let myVar: StringOrNumber; type StringOrNumber string | number; const x: Foo {}; interface Foo {}从实现看这一分支对应shouldCheck中的options.ignoreTypeReferences (referenceContainsTypeQuery(identifier) || identifier.parent.type TSTypeReference)判断lib/rules/no-use-before-define.js。其中referenceContainsTypeQuery辅助函数lib/rules/no-use-before-define.js沿 AST 向上回溯专门识别TSTypeQuery即typeof X类型语法与TSQualifiedName嵌套链。nofuncnofunc是最常用的字符串简写语义为“只放行函数声明其余全部严格检查”。它等价于{ functions: false, classes: true, variables: true, allowNamedExports: false, enums: true, typedefs: true, ignoreTypeReferences: true }。nofunc下的错误示例JavaScript/*eslint no-use-before-define: [error, nofunc]*/ a(); var a function() {}; console.log(foo); var foo 1; function f() { return b; } var b 1; new A(); class A { } function g() { return new B(); } class B { } export default bar; const bar 1; export { baz }; const baz 1;nofunc下的错误示例TypeScript/*eslint no-use-before-define: [error, nofunc]*/ function foo(): Foo { return Foo.FOO; } enum Foo { FOO, }nofunc下的正确示例JavaScript——函数声明可以前置调用但类、变量、导出引用仍须遵守先声明后使用/*eslint no-use-before-define: [error, nofunc]*/ f(); function f() {} class A { } new A(); var a 10; alert(a); const foo 1; export { foo }; const bar 1; export default bar;nofunc下的正确示例TypeScript/*eslint no-use-before-define: [error, nofunc]*/ enum Foo { FOO, } const foo Foo.Foo;源码级原理规则内部的工作流程把源码 lib/rules/no-use-before-define.js 通读一遍可以还原出该规则完整的工作流水线入口create(context)先通过parseOptions解析出最终选项对象然后向Program节点注册监听器在程序入口处调用checkReferencesInScope(sourceCode.getScope(node))lib/rules/no-use-before-define.js。作用域数据由 ESLint 内置的eslint-scope在解析阶段构建完成。递归遍历作用域checkReferencesInScopelib/rules/no-use-before-define.js对当前作用域的所有references过滤出需要检查的引用再递归处理每个子作用域。引用筛选shouldChecklib/rules/no-use-before-define.js依次排除以下情况reference.init为真即该引用出现在某个变量的初始化器中如let a 1中对a的引用未解析的引用!variable——此时规则无从判断“声明位置”例如全局环境变量、函数内arguments等allowNamedExports豁免的ExportSpecifier引用按选项关闭的FunctionName、Variable、ClassName、TSEnumName、Type定义类型ignoreTypeReferences下的类型引用TSTypeReference或typeof类型查询TSQualifiedName中非最左端的嵌套命名空间别名位于类装饰器中的类引用isClassRefInClassDecoratorlib/rules/no-use-before-define.js——因为装饰器在转译后会被放到类声明之后属于安全引用。位置比较与初始化判定通过筛选的引用会被比较引用位置与定义位置的range——若引用的range[1]小于定义标识符的range[1]即引用在文本上先出现或者该引用发生在变量自身初始化期间isEvaluatedDuringInitializationlib/rules/no-use-before-define.js涵盖var a a、解构默认值、for-in/of右侧、class C extends C、类的静态字段初始化器等场景则报告usedBeforeDefined消息。值得注意的是isEvaluatedDuringInitialization对“类绑定在静态初始化器运行前已初始化”这一语义做了精细处理class C { static foo C; static { bar C; } }是合法的因为类绑定先于静态字段与静态块执行所以只有当引用位置落在类静态初始化器静态块或静态字段的初始值范围内时才判定为违规参见isInClassStaticInitializerRangelib/rules/no-use-before-define.js的区间检查。整套行为在 tests/lib/rules/no-use-before-define.js 中有超过三千行的回归测试背书覆盖了 ES5 到 ES2022 的语法ecmaVersion从 5 到 2022、nofunc字符串选项、typedefs/enums/ignoreTypeReferences的每种排列组合以及类静态块、解构默认值、命名导出等边界场景是理解规则预期行为的另一份权威参考。实战配置建议综合以上分析给出三种典型场景的配置建议追求最严格、最安全保持默认配置即可或显式写出全部选项所有绑定一律先声明后使用最贴近 TDZ 的运行时语义适合对代码可读性要求高的团队。允许函数提升的惯用法如果代码中大量使用相互递归、或者依赖函数声明提升的组织方式推荐[error, nofunc]既保留了函数声明的灵活性又对变量、类、导出保持严格检查。与模块导出配合如果项目大量使用“先集中export、后定义”的组织风格文件顶部先列出导出清单可启用{ allowNamedExports: true }它能在不影响安全性的前提下减少噪音。在扁平配置flat config下的完整启用示例// eslint.config.js export default [ { rules: { no-use-before-define: [error, { functions: false, // 允许函数声明提升 classes: true, variables: true, allowNamedExports: true, enums: true, typedefs: true, ignoreTypeReferences: true }] } } ];需要留意的是当前仓库中的 ESLint 版本默认不推荐此规则recommended: false但它与no-undef等规则互补no-undef负责报告完全未声明的标识符no-use-before-define则负责报告“已声明但声明得太晚”的标识符。建议在启用前结合团队代码风格选择functions与variables的取舍因为这两项直接决定了规则会与哪些 JavaScript 惯用法冲突。【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表