ESLint no-var 规则详解:用 let 与 const 全面取代 var
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
ESLint 的no-var规则(建议类规则,rule_type: suggestion)旨在全面禁止使用var声明变量,引导开发者改用 ES6(ECMAScript 6)引入的let与const块级作用域声明。本文将基于本仓库(eslint 项目)中的 规则文档 与 规则实现源码,系统讲解no-var的判定逻辑、可自动修复范围、TypeScript 场景支持以及迁移注意事项,帮助你安全地把旧代码中的var批量升级为let。
为什么需要 no-var:函数作用域带来的经典陷阱
ECMAScript 6 允许程序员使用let和const关键字创建块级作用域(block scope)变量,而非var的函数作用域(function scope)。块级作用域在许多其他编程语言中很常见,能帮助程序员避免一类典型错误:块内声明意外覆盖外层同名变量。
以规则文档给出的示例为例:
var count = people.length; var enoughFood = sandwiches.length >= count; if (enoughFood) { var count = sandwiches.length; // accidentally overriding the count variable console.log("We have " + count + " sandwiches for everyone. Plenty for all!"); } // our count variable is no longer accurate console.log("We have " + count + " people and " + sandwiches.length + " sandwiches!");由于var声明会被提升(hoisting)到函数作用域顶部,if块内的var count与函数外层count是同一个变量,第二次赋值后原始计数被永久破坏。若改用let/const,块内声明会形成独立绑定,外层值不受影响。no-var规则正是为了在代码层面杜绝这类隐患。
Rule Details:规则目标与判定标准
no-var规则的目标是阻止var的使用,鼓励改用const或let。从实现上看,该规则在VariableDeclaration节点退出时触发检查:只要node.kind === "var"就报告错误(见 lib/rules/no-var.js#L470-L485),报告消息为"Unexpected var, use let or const instead."(见 lib/rules/no-var.js#L317-L319)。
在 rules/index.js 中该规则以懒加载方式注册,可在 ESLint 配置中通过"no-var": "error"、"warn"或"off"三种严重级别启用。
不正确的代码示例(将被报告)
/*eslint no-var: "error"*/ var x = "y"; var CONFIG = {};正确的代码示例(通过检查)
/*eslint no-var: "error"*/ let x = "y"; const CONFIG = {};需要注意的是:规则只要求消除var,至于应该用let还是const,取决于变量是否会被重新赋值——const适用于从不改变的绑定(如上述CONFIG)。文档中的示例以/*eslint no-var: "error"*/行内注释形式展示,实际项目中通常将规则写入配置文件(如eslint.config.js)。
TypeScript 场景支持
no-var规则额外支持 TypeScript 类型语法。在 TypeScript 中有多种声明全局变量的方式,其中只有var适用于所有场景,因此本规则针对 TypeScript 的declare声明也有专门处理。
不正确的 TypeScript 代码示例
/*eslint no-var: "error"*/ declare var x: number declare namespace ns { var x: number } declare module 'module' { var x: number }正确的 TypeScript 代码示例
/*eslint no-var: "error"*/ declare global { declare var x: number }从源码看,规则对 TypeScript 有一个专门豁免:当var声明位于declare global块(即TSModuleBlock的父节点是TSModuleDeclaration且global为真)时直接跳过检查(见 lib/rules/no-var.js#L476-L483)。这是因为在declare global环境中,declare var是唯一合法的全局声明方式。对应的测试用例(见 tests/lib/rules/no-var.js#L600-L618)也验证了declare global { var bar: 'car' }是合法代码,而declare var x、declare namespace ns { var ... }、declare module 'module' { var ... }均会被报告并可修复为let。
Options:无配置项
no-var规则没有选项。其schema为空数组(见 lib/rules/no-var.js#L314),启用时只需配置严重级别即可,例如:
// eslint.config.js export default [ { rules: { "no-var": "error", }, }, ];自动修复:何时可以安全地把 var 改成 let
no-var是可自动修复的规则(fixable: "code",见 lib/rules/no-var.js#L315)。但var与let的语义差异远不止作用域关键字本身——var允许重声明、允许提升后提前引用、循环中共享同一实例,而let禁止这一切。因此源码中的canFix()函数(见 lib/rules/no-var.js#L402-L446)实现了大量安全检查,以下情况只报告、不修复(测试覆盖见 tests/lib/rules/no-var.js):
1. 全局变量或变量被重声明
var允许在同一作用域重复声明,而let的重声明是语法错误,因此当变量是全局变量(isGlobal)或被重声明(isRedeclared,即variable.defs.length >= 2,见 lib/rules/no-var.js#L121-L123)时不修复。测试用例var a, b, c; var a;与var a; if (b) { var a; }均输出null(不修复),但每个var都会被报告(tests/lib/rules/no-var.js#L169-L198)。
2. 变量在声明之前被引用
var的声明提升允许"先使用后声明",let则不行(会触发暂时性死区 TDZ)。源码通过hasReferenceBeforeDeclaration检查引用位置是否早于声明位置(lib/rules/no-var.js#L208-L222)。测试用例function test() { console.log(x); var x = 1; }因此不会被修复(tests/lib/rules/no-var.js#L459-L463)。
3. 变量被提升的函数声明在声明前调用
hasUnsafeHoistedFunctionReference专门处理"提升的函数声明在变量声明之前被调用、且该函数内部引用了该变量"的场景(lib/rules/no-var.js#L231-L272)。例如function wrap() { foo(); var a = 1; function foo() { console.log(a); } }不修复,因为foo()调用时a还未被赋值;而var a = 1在foo()之前初始化时则可以修复(见 tests/lib/rules/no-var.js#L562-L569)。
4. 变量逃逸出所在作用域
isUsedFromOutsideOf检查变量是否在声明所在块之外被引用(lib/rules/no-var.js#L131-L148)。if (foo) { var a, b, c; } a;、for (var i = 0; i < 10; ++i) {} i;等用例均不修复(tests/lib/rules/no-var.js#L191-L222),因为改成let后外部引用会抛 ReferenceError。
5. 声明位于 SwitchCase 节点上
若修改switch的case分支中的var,会触发no-case-declarations规则警告,而eslint:recommended预置集包含该规则,因此这类声明不修改(见 lib/rules/no-var.js#L355-L361 的注释说明,以及node.parent.type === "SwitchCase"检查)。测试用例switch (a) { case 0: var b = 1 }不修复(tests/lib/rules/no-var.js#L223-L230)。
6. 变量存在 TDZ 自引用
var a = a、var {a = a} = {}这类声明在初始化器中引用自身,改成let会触发 TDZ 错误。hasSelfReferenceInTDZ与hasReferenceInTDZ负责检测(lib/rules/no-var.js#L162-L190、lib/rules/no-var.js#L330-L337)。测试用例var a = a、var {a = a} = {}、var a = b, b = 1均不修复(tests/lib/rules/no-var.js#L250-L294)。
7. 循环中被闭包引用或可能未初始化
循环内var声明在所有迭代间共享同一变量实例,而let每次迭代创建新实例,行为差异可能不安全。源码检查:
isReferencedInClosure:变量被更具体的函数表达式(闭包)引用时不修复(lib/rules/no-var.js#L57-L65)。用例for (var a of b) { arr.push(() => a); }不修复(tests/lib/rules/no-var.js#L233-L240)。- 循环内
let声明无初始化器时初始化为null,而var会保留上一轮迭代的值。只有当变量是循环赋值目标(isLoopAssignee,如for (var a of b))或声明带初始化器时才安全转换(lib/rules/no-var.js#L424-L431)。用例for (let a of b) { var c; console.log(c); c = 'hello'; }不修复(tests/lib/rules/no-var.js#L241-L248)。
值得注意的是,for (var a of b)、for (var a in b)、for (var i = 0; ...)这类循环头部的var是可以修复为let的(见 tests/lib/rules/no-var.js#L92-L139 的output断言),这也是最推荐的现代写法。
8. 声明不在语句列表位置
规则只修复位于语句列表中的声明。if (foo) var bar = 1;这种单行声明不在块内,let写法会改变作用域语义,因此不修复(lib/rules/no-var.js#L433-L443,对应测试 tests/lib/rules/no-var.js#L322-L334)。语句列表父节点集合STATEMENT_LIST_PARENTS定义于 lib/rules/utils/ast-utils.js#L50-L56,包括Program、BlockStatement、StaticBlock、SwitchCase,本规则额外补充了TSModuleBlock。
9. 变量名为let或与 catch 参数冲突
- 变量名为
let(如function foo() { var let; })时,改成let let是语法错误,不修复(hasNameDisallowedForLetDeclarations,lib/rules/no-var.js#L198-L200;测试见 tests/lib/rules/no-var.js#L371-L381)。 var允许重用简单 catch 子句参数的名字,但let不允许:在 catch 块内直接声明是语法错误,在嵌套块内则行为改变(var赋值给 catch 参数,let创建新绑定)。isShadowedByCatchParameter专门检测此场景(lib/rules/no-var.js#L292-L296)。用例try {} catch (e) { var e = 1; }不修复,而try {} catch (e) { var x = 1; }正常修复(tests/lib/rules/no-var.js#L509-L552)。
10. 变量在函数表达式内部被自引用
var foo = (function () { foo(); })();这类在立即调用函数表达式(IIFE)内部自引用的场景,改成let会因 TDZ 报错而不修复;但普通命名函数表达式赋值如var fx = function (i = 0) { ... return fx(...) }可以安全修复(见 tests/lib/rules/no-var.js#L383-L433)。
修复机制:只替换关键字,不动声明结构
当canFix返回true时,修复逻辑极其简单——找到声明节点的var关键字 token,整体替换为let(lib/rules/no-var.js#L453-L468):
fix(fixer) { const varToken = sourceCode.getFirstToken(node, { filter: t => t.value === "var", }); return canFix(node) ? fixer.replaceText(varToken, "let") : null; }也就是说,no-var的自动修复统一替换为let,不会自作主张推断该用const。例如var foo = bar, toast = most;修复为let foo = bar, toast = most;(tests/lib/rules/no-var.js#L68-L79),var CONFIG = {}也会被修复为let CONFIG = {},是否进一步改为const由开发者自行决定(可通过prefer-const规则配合处理)。
与相关规则的配合
- prefer-const:
no-var只负责消除var,而prefer-const规则负责把从未重新赋值的let进一步升级为const,两者配合可实现"声明从不改变 → const,否则 → let"的完整现代化迁移。 - no-case-declarations:
eslint:recommended预置集(tests/conf/eslint-recommended.js)中的规则,no-var为避免与它冲突,特意不修复switchcase 分支内的var声明。 - prefer-arrow-callback:与
no-var同属 ECMAScript 6 现代化迁移规则族,常一起启用。
在 rule-type-list.json 的规则类型分类中,no-var属于suggestion类型(非 problem、非 layout),表示它更多是风格与最佳实践层面的建议,而非会破坏运行时行为的问题规则。
When Not To Use It:何时应关闭此规则
在以下场景应考虑关闭或调整no-var:
- 非 ES6 环境:需要支持不支持
let/const的旧运行时(如老旧浏览器、低版本 Node.js)时,应关闭此规则。 - 存量项目渐进迁移成本过高:已有 JavaScript 项目刚开始引入 ES6 时,若从
var到let的迁移代价过大,可能不希望立即应用此规则。尤其是涉及上面列举的不修复场景(重声明、提升依赖、闭包捕获等)时,手动改造需要逐一确认行为差异。 - TypeScript 的
declare global场景:如本文所述,该场景下var是唯一合法的全局声明方式,规则已自动豁免。
建议的落地策略是:在新代码中直接启用no-var: "error",对存量代码使用--fix自动修复安全部分,再结合代码审查逐步处理剩余的不修复场景。
总结
no-var是 ES6 代码现代化中最基础也最安全的规则之一:它无配置项、语义清晰、可自动修复,其核心价值在于利用块级作用域从源头避免var的作用域提升与变量覆盖问题。理解其canFix中十余种"只报告不修复"的边界场景(全局/重声明/逃逸作用域/TDZ/闭包/循环/catch 参数冲突等),能帮助你在批量迁移时准确预判哪些代码需要人工介入,真正做到安全、高效地把var全面替换为let与const。
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考