ESLint use-isnan 规则全解析:如何彻底禁止与 NaN 直接比较
2026/9/13 10:33:54 网站建设 项目流程

ESLint use-isnan 规则全解析:如何彻底禁止与 NaN 直接比较

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

use-isnan是 ESLint 内置的problem类型规则,其核心职责是拦截 JavaScript 代码中所有与NaN直接比较的写法(如foo === NaNfoo != Number.NaNcase NaNindexOf(NaN)等),并引导开发者改用isNaN()/Number.isNaN()语义正确的判断方式。本文以仓库内的规则文档 docs/src/rules/use-isnan.md 为骨架,结合 lib/rules/use-isnan.js 源码实现与 tests/lib/rules/use-isnan.js 测试用例,系统讲解 NaN 比较陷阱、规则的两个可配置项、可用的代码修复建议以及背后的 AST 检测原理,帮助你在实际项目中正确开启并运用该规则。

为什么与 NaN 比较是陷阱:先理解 NaN 的语义

在 JavaScript 中,NaN(Not-a-Number)是Number类型的一个特殊值,它用来表示 IEEE 754 双精度 64 位浮点数格式中任何"非数字"的结果。NaN在 JavaScript 中独一无二的地方在于:它不等于任何东西,包括它自己。因此所有与NaN的比较结果都令人困惑:

  • NaN === NaNNaN == NaN计算结果为false
  • NaN !== NaNNaN != NaN计算结果为true

也就是说,用===判断一个值"是否为 NaN"永远得不到预期的答案。正确的做法是调用Number.isNaN()或全局isNaN()函数来判断一个值是否为NaN,这正是本规则存在的意义。

补充说明:全局isNaN()Number.isNaN()在语义上并不完全等价。全局isNaN()会先对参数执行 Number 类型强制转换,因此isNaN("abc")返回true;而Number.isNaN()不做任何强制转换,Number.isNaN("abc")返回false。这一差异直接影响了规则提供的修复建议(详见下文"修复建议"一节)。

Rule Details:规则禁止什么

use-isnan规则禁止与NaN进行任何比较。被禁止的比较对象包括两类写法:

  • 全局标识符NaN
  • 全局Number对象上的Number.NaN属性

规则的错误示例(会被报告为comparisonWithNaN):

/*eslint use-isnan: "error"*/ if (foo == NaN) { // ... } if (foo != NaN) { // ... } if (foo == Number.NaN) { // ... } if (foo != Number.NaN) { // ... }

规则的正确示例:

/*eslint use-isnan: "error"*/ if (isNaN(foo)) { // ... } if (!isNaN(foo)) { // ... }

需要指出的是,规则不仅拦截==/!=/===/!==这类等值比较,还会拦截<><=>=这类关系比较。这一点从源码中的检测正则可以看出(lib/rules/use-isnan.js):

if ( /^(?:[<>]|[!=]=)=?$/u.test(node.operator) && (isNaNIdentifier(node.left, sourceCode) || isNaNIdentifier(node.right, sourceCode)) ) {

该正则可以匹配=====!=!==<><=>=共 8 种运算符,只要BinaryExpression(二元表达式)的任意一侧是 NaN 标识,就会被报告。测试用例也验证了这一点,例如NaN < "abc""abc" >= NaN等写法都会被标记为错误,只不过这类关系比较不会提供修复建议(suggestions: [],参见 tests/lib/rules/use-isnan.js)。

规则对"真正的 NaN"识别非常严格

从源码的isNaNIdentifier辅助函数(lib/rules/use-isnan.js)可以看出,规则对 NaN 的识别有几条严格约束,避免误报:

  • 只认全局的NaN标识符与全局Number上的NaN属性,检测通过sourceCode.isGlobalReference()完成,因此被遮蔽(shadowed)的NaNNumber不会被误报,例如let NaN; if (x === NaN) {}是合法的(测试用例见 tests/lib/rules/use-isnan.js);
  • Number.NaN会通过skipChainExpression展开可选链,因此x === Number?.NaN同样会被捕获(lib/rules/use-isnan.js);
  • SequenceExpression(逗号表达式)会取最后一个表达式判断,因此x === (doStuff(), NaN)会被报错,而x === (NaN, 1)(NaN 不是最后一个值)则不会;
  • Number[NaN](计算属性,属性名本身是 NaN)不属于被禁止的比较,因为此时比较对象不是Number.NaN属性值。

Options:规则的两个可配置项

use-isnan接受一个对象类型的选项,包含两个布尔开关:

选项默认值作用
enforceForSwitchCasetrue额外禁止switch语句中的case NaNswitch(NaN)
enforceForIndexOffalse额外禁止以NaN为参数调用indexOflastIndexOf

这两个选项的默认值在源码的defaultOptions中声明(lib/rules/use-isnan.js):

defaultOptions: [ { enforceForIndexOf: false, enforceForSwitchCase: true, }, ],

规则内部则根据解析后的选项动态注册 AST 监听器(lib/rules/use-isnan.js):BinaryExpression监听器始终注册;仅当enforceForSwitchCase为真时注册SwitchStatement监听器;仅当enforceForIndexOf为真时注册CallExpression监听器。

enforceForSwitchCase:switch 里的 NaN 永远匹配不上

switch语句内部使用===(严格相等)来匹配表达式的值与各个 case 子句。由于NaN不等于任何值(包括它自己),所以:

  • case NaN永远不可能匹配成功;
  • switch(NaN)永远不可能命中任何 case 子句。

因此当"enforceForSwitchCase"设为true(默认)时,下面这些写法都会被禁止。错误示例

/*eslint use-isnan: ["error", {"enforceForSwitchCase": true}]*/ switch (foo) { case NaN: bar(); break; case 1: baz(); break; // ... } switch (NaN) { case a: bar(); break; case b: baz(); break; // ... } switch (foo) { case Number.NaN: bar(); break; case 1: baz(); break; // ... } switch (Number.NaN) { case a: bar(); break; case b: baz(); break; // ... }

正确示例(先用Number.isNaN判断,再进入 switch):

/*eslint use-isnan: ["error", {"enforceForSwitchCase": true}]*/ if (Number.isNaN(foo)) { bar(); } else { switch (foo) { case 1: baz(); break; // ... } } if (Number.isNaN(a)) { bar(); } else if (Number.isNaN(b)) { baz(); } // ...

如果确实需要容忍switch中存在 NaN 相关写法,可以显式关闭该选项:

/*eslint use-isnan: ["error", {"enforceForSwitchCase": false}]*/ switch (foo) { case NaN: bar(); break; case 1: baz(); break; // ... } switch (NaN) { case a: bar(); break; case b: baz(); break; // ... } switch (foo) { case Number.NaN: bar(); break; case 1: baz(); break; // ... } switch (Number.NaN) { case a: bar(); break; case b: baz(); break; // ... }

源码中checkSwitchStatement(lib/rules/use-isnan.js)分别检查switch的判别式(discriminant)与每个case的测试表达式(test),命中后报告两条不同的消息:

  • switchNaN'switch(NaN)' can never match a case clause. Use Number.isNaN instead of the switch.
  • caseNaN'case NaN' can never match. Use Number.isNaN before the switch.

值得注意的细节是:同一段代码可能同时触发两条报告,例如switch(NaN) { case NaN: break; }会同时报告switchNaNcaseNaN两个错误(对应测试见 tests/lib/rules/use-isnan.js)。

enforceForIndexOf:indexOf / lastIndexOf 永远找不到 NaN

Array.prototype.indexOfArray.prototype.lastIndexOf内部同样使用===严格比较来匹配给定值与数组元素,因此对任意数组foofoo.indexOf(NaN)foo.lastIndexOf(NaN)永远返回-1,即"永远找不到 NaN"。这种写法在逻辑上是必然失败的死代码,所以本选项应运而生。

"enforceForIndexOf"设为true时,规则会报告indexOf(NaN)lastIndexOf(NaN)这类方法调用。错误示例

/*eslint use-isnan: ["error", {"enforceForIndexOf": true}]*/ const hasNaN = myArray.indexOf(NaN) >= 0; const firstIndex = myArray.indexOf(NaN); const lastIndex = myArray.lastIndexOf(NaN); const indexWithSequenceExpression = myArray.indexOf((doStuff(), NaN)); const firstIndexFromSecondElement = myArray.indexOf(NaN, 1); const lastIndexFromSecondElement = myArray.lastIndexOf(NaN, 1);

正确示例(手写遍历或用some/findIndex/includes等不依赖===匹配 NaN 的 API):

/*eslint use-isnan: ["error", {"enforceForIndexOf": true}]*/ function myIsNaN(val) { return typeof val === "number" && isNaN(val); } function indexOfNaN(arr) { for (let i = 0; i < arr.length; i++) { if (myIsNaN(arr[i])) { return i; } } return -1; } function lastIndexOfNaN(arr) { for (let i = arr.length - 1; i >= 0; i--) { if (myIsNaN(arr[i])) { return i; } } return -1; } const hasNaN = myArray.some(myIsNaN); const hasNaN1 = indexOfNaN(myArray) >= 0; const firstIndex = indexOfNaN(myArray); const lastIndex = lastIndexOfNaN(myArray); // ES2015 const hasNaN2 = myArray.some(Number.isNaN); // ES2015 const firstIndex1 = myArray.findIndex(Number.isNaN); // ES2016 const hasNaN3 = myArray.includes(NaN);

从源码看,checkCallExpression(lib/rules/use-isnan.js)对检测条件做了精细控制:

  • callee 必须是MemberExpression(成员表达式),属性名通过getStaticPropertyName静态解析为indexOflastIndexOf
  • 实参数量不能超过 2 个,且第一个实参必须是 NaN 标识;
  • 不要求对象真实是数组(这正是文档"Known Limitations"所指出的:规则按方法名匹配,即使调用方对象不是数组也会报告)。

已知限制(Known Limitations)enforceForIndexOf只按方法名检查,即使持有该方法的对象并非数组,也会被报告。例如自定义对象上的同名方法若接收 NaN 参数,同样会被该选项拦截,需要自行权衡。

修复建议(Suggestions):规则自带两种自动修复思路

规则的meta声明了hasSuggestions: true(lib/rules/use-isnan.js),意味着它不会通过--fix静默自动修复,而是在 IDE 或编辑器中提供"快速修复"建议,由开发者显式选择接受。这保证了语义上的安全性。

二元比较的修复建议

对于==/===/!=/!==这类可修复的等值比较(fixableOperators),规则提供如下建议(getBinaryExpressionFixer的实现见 lib/rules/use-isnan.js):

  • replaceWithIsNaN(Replace with Number.isNaN):例如123 === NaN建议改为Number.isNaN(123);对!==/!=会生成取反形式,如123 !== NaN建议改为!Number.isNaN(123)
  • replaceWithCastingAndIsNaN(Replace with Number.isNaN and cast to a Number):仅针对宽松等值==/!=castableOperators)额外提供,例如123 == NaN建议改为Number.isNaN(Number(123))。原因是==本身包含隐式类型转换语义,先用Number()显式转换可以尽量保持原表达式的行为。

以测试用例为例(tests/lib/rules/use-isnan.js):

// 输入:123 == NaN // 建议1:Number.isNaN(123); // 建议2:Number.isNaN(Number(123)); // 输入:123 === NaN // 建议:Number.isNaN(123);

对于<><=>=关系比较以及涉及SequenceExpression(如x === (doStuff(), NaN))的情况,规则只报告错误而不提供修复建议,以避免破坏表达式中可能存在的副作用。

indexOf / lastIndexOf 的修复建议

enforceForIndexOf开启且写法可安全替换时,规则提供replaceWithFindIndex建议(lib/rules/use-isnan.js):

  • myArray.indexOf(NaN)建议改为myArray.findIndex(Number.isNaN)
  • myArray.lastIndexOf(NaN)建议改为myArray.findLastIndex(Number.isNaN)

其前提是第一个实参不是SequenceExpression且不存在第二个参数(起始下标)。测试也验证了计算属性写法会被正确改写,例如foo'indexOf'建议为foo"findIndex"(tests/lib/rules/use-isnan.js)。

在配置文件中启用 use-isnan

use-isnan在规则元数据中标记为recommended: true(lib/rules/use-isnan.js),属于官方推荐开启的"问题发现"类规则。在 ESLint 的扁平化配置(flat config)中可以直接这样启用:

// eslint.config.js export default [ { rules: { "use-isnan": "error", // 或者开启附加检查 "use-isnan": ["error", { enforceForSwitchCase: true, // 默认即 true enforceForIndexOf: true, // 默认 false,按需开启 }], }, }, ];

规则的完整校验模式(schema)在源码中声明(lib/rules/use-isnan.js):选项必须是对象,仅允许enforceForSwitchCaseenforceForIndexOf两个布尔属性,出现其他多余属性会直接报配置错误(additionalProperties: false)。

源码结构:规则是如何被加载与注册的

use-isnan是 ESLint 内置核心规则之一,与其他规则一样通过 lib/rules/index.js 统一注册:

"use-isnan": () => require("./use-isnan"),

其 AST 检测逻辑依赖 lib/rules/utils/ast-utils.js 中提供的基础工具函数,包括:

  • isSpecificId(ast-utils.js):判断节点是否为指定名称的标识符;
  • isSpecificMemberAccess(ast-utils.js):判断是否为指定对象/属性的成员访问;
  • getStaticPropertyName(ast-utils.js):静态解析成员属性名,支持点号、方括号与模板字符串写法;
  • skipChainExpression(ast-utils.js):展开可选链表达式,保证foo?.indexOf(NaN)Number?.NaN也能被正确识别。

总结

use-isnan是 ESLint 中针对NaN误用场景最直接的一道防线:它把"与 NaN 比较"这一类在语义上必然失败或结果反直觉的代码(===/==/!==/!=/ 关系比较、switch分支、indexOf/lastIndexOf查找)统一拦截,并通过 IDE 修复建议引导开发者迁移到isNaN()/Number.isNaN()findIndex/includes等正确写法。开启时建议根据项目实际场景决定enforceForSwitchCaseenforceForIndexOf的取值,其中后者需要注意其"按方法名匹配、不校验调用对象是否为数组"的已知限制。结合本仓库的 规则文档、实现源码 与 测试用例,你可以完整掌握该规则的检测边界与修复行为,将其安全地落地到团队代码规范中。

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

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

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

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

立即咨询