ESLint no-use-before-define 规则深度解析:如何根除 JavaScript/TypeScript 中的“先使用后声明”
2026/9/12 23:04:11 网站建设 项目流程

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

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

no-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,系统讲解该规则的检查逻辑、全部配置项(functionsclassesvariablesallowNamedExports,以及 TypeScript 专属的enumstypedefsignoreTypeReferences)与"nofunc"简写,并深入源码剖析其作用域分析、暂时性死区判断与类静态初始化器等底层实现原理。读完本文,你将能精确配置该规则以适配团队编码风格,并理解它为什么能覆盖varletconst、函数、类、class 静态块、ES Module 具名导出与 TypeScript 类型声明等全部场景。

规则背景:提升(Hoisting)与暂时性死区(TDZ)

在 ES6 之前,JavaScript 中的变量声明与函数声明会被提升到其所在作用域的顶部,因此在代码中“先使用、后声明”是语法上合法的行为。例如:

alert(a); // 可以运行,a 为 undefined var a = 10;

这种写法虽然能运行,却容易让人困惑——代码阅读者很难判断a此刻到底是undefined、全局变量还是尚未初始化,于是许多团队约定“先声明、后使用”。

ES6 引入块级绑定(letconst)之后,情况变得更加严格:在声明语句执行之前访问该绑定会触发暂时性死区(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)、块级let(TDZ)、类的自引用与继承(class C extends C {})、class 静态字段与静态块中对类名自身的引用(static x = Cstatic { 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; }合法,因为实例字段初始化器是在实例化时才执行的“隐式函数”,属于独立的执行上下文;而静态字段在类定义求值阶段就会运行。
  • exportimport的对称性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 {};中的引用
enumsbooleantrue(TypeScript)是否检查enum被提前引用
typedefsbooleantrue(TypeScript)是否检查type别名 /interface被提前引用
ignoreTypeReferencesbooleantrue(TypeScript)是否忽略类型注解、类型断言等纯类型位置上的引用

此外规则还接受字符串选项"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 中,供文档站点自动渲染。

functions

functions决定规则是否检查函数声明被提前引用:

  • 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管辖。

classes

classes决定规则是否检查上层作用域中的类声明被提前引用:

  • 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,代表执行上下文)是否一致,只有引用确实来自独立的执行上下文时,才允许关闭对应选项。

variables

variables决定规则是否检查上层作用域中的变量声明被提前引用:

  • 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 = () => {};fconst绑定的函数表达式,属于变量而非函数声明,仍受variables约束,故报错;
  • 类实例字段x = foo;是隐式函数(实例化时才执行),属于独立执行上下文,可放行;而静态字段static x = foo;与静态块static { this.x = foo; }在类定义求值阶段即运行,属于父执行上下文,即使variables: false也照常报错。

上述行为在源码中由两个辅助函数精确建模:isClassStaticInitializerScope(lib/rules/no-use-before-define.js)识别class-static-block与静态字段初始化器(class-field-initializer且对应PropertyDefinition.static === true)这两类特殊作用域;isFromSeparateExecutionContext则在向上寻找变量作用域的过程中,把“类静态初始化器”当作父执行上下文的一部分(因为它们在类定义求值期间自动运行),其余跨越函数边界的情况一律判定为独立执行上下文。

allowNamedExports

allowNamedExports若设为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):当allowNamedExportstrue且标识符的父节点是ExportSpecifier且标识符就是该导出说明符的local端时,直接返回false(跳过检查);export default a的标识符父节点是ExportDefaultDeclaration而非ExportSpecifier,所以不受此豁免。

TypeScript 扩展:enums / typedefs / ignoreTypeReferences

规则在默认配置下同样覆盖 TypeScript 的enumtype别名与interface,并额外提供三个选项细化控制。启用 TypeScript 检查时,需要把对应文件交给启用了 TypeScript 解析器的 ESLint 实例处理(例如在languageOptions.parser中配置@typescript-eslint/parser)。

enums(TypeScript only)

enumstrue(默认)时,规则会检查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)。

typedefs(TypeScript only)

typedefstrue(默认)时,规则会检查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 === "Type"(lib/rules/no-use-before-define.js)。

ignoreTypeReferences(TypeScript only)

ignoreTypeReferencestrue(默认)时,规则会忽略所有纯类型位置上的引用,例如类型注解、类型断言(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: falsetypedefs: 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嵌套链。

nofunc

"nofunc"是最常用的字符串简写,语义为“只放行函数声明,其余全部严格检查”。它等价于{ "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 通读一遍,可以还原出该规则完整的工作流水线:

  1. 入口create(context)先通过parseOptions解析出最终选项对象,然后向Program节点注册监听器,在程序入口处调用checkReferencesInScope(sourceCode.getScope(node))(lib/rules/no-use-before-define.js)。作用域数据由 ESLint 内置的eslint-scope在解析阶段构建完成。
  2. 递归遍历作用域checkReferencesInScope(lib/rules/no-use-before-define.js)对当前作用域的所有references过滤出需要检查的引用,再递归处理每个子作用域。
  3. 引用筛选shouldCheck(lib/rules/no-use-before-define.js)依次排除以下情况:
    • reference.init为真:即该引用出现在某个变量的初始化器中(如let a = 1中对a的引用);
    • 未解析的引用(!variable)——此时规则无从判断“声明位置”,例如全局环境变量、函数内arguments等;
    • allowNamedExports豁免的ExportSpecifier引用;
    • 按选项关闭的FunctionNameVariableClassNameTSEnumNameType定义类型;
    • ignoreTypeReferences下的类型引用(TSTypeReferencetypeof类型查询);
    • TSQualifiedName中非最左端的嵌套命名空间别名;
    • 位于类装饰器中的类引用(isClassRefInClassDecorator,lib/rules/no-use-before-define.js)——因为装饰器在转译后会被放到类声明之后,属于安全引用。
  4. 位置比较与初始化判定:通过筛选的引用会被比较引用位置与定义位置的range——若引用的range[1]小于定义标识符的range[1](即引用在文本上先出现),或者该引用发生在变量自身初始化期间isEvaluatedDuringInitialization,lib/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; } }是合法的,因为类绑定先于静态字段与静态块执行,所以只有当引用位置落在类静态初始化器(静态块或静态字段的初始值)范围内时才判定为违规,参见isInClassStaticInitializerRange(lib/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则负责报告“已声明但声明得太晚”的标识符。建议在启用前结合团队代码风格选择functionsvariables的取舍,因为这两项直接决定了规则会与哪些 JavaScript 惯用法冲突。

【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询