ESLint no-native-reassign 规则深度解析:禁止覆盖只读全局变量及弃用迁移指南
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
本文围绕 ESLint 核心规则no-native-reassign展开,讲解它如何防止开发者误覆写window、Object、undefined等内置全局变量,并通过源码级分析揭示其基于"只读全局变量"与"写引用"的检测原理。读完本文,你将掌握该规则的完整配置方法(含exceptions选项)、它与no-undef、no-global-assign的关联脉络,以及从no-native-reassign平滑迁移到no-global-assign的实操方案。
为什么不能给内置全局变量赋值
JavaScript 运行环境内置了大量全局变量,例如浏览器中的window、Node.js 中的process。绝大多数情况下,你不应该给这些全局变量重新赋值——一旦覆盖,就可能永久丢失对关键功能的访问。例如在浏览器代码中,下面的写法几乎总是错误:
window = {};window这种例子比较显而易见,但实际环境中内置全局对象往往数以百计,开发者很难逐一判断"我此刻赋值的名字是不是某个内置全局变量"。no-native-reassign规则正是为这个问题而生:它利用 ESLint 对全局变量"可写/只读"状态的跟踪能力,自动拦截对只读全局变量的修改。
从版本演化看,本规则从 v0.0.9 起随 ESLint 存在(见 docs/src/_data/rule_versions.json),其历史可以追溯到它原先作为no-undef规则的一部分(见 docs/src/use/migrating-to-3.0.0.md)。
规则详情:它到底检测什么
规则的核心声明是:禁止修改只读(read-only)的全局变量(原文见 docs/src/rules/no-native-reassign.md)。这里的关键在于"只读"二字——ESLint 本身具备把全局变量配置为只读的能力,规则针对的不仅是原生内置对象,也包括任何被显式声明为只读的全局变量。
源码级原理:从Program节点到Reference
打开规则实现 lib/rules/no-native-reassign.js,可以看到检测逻辑非常精巧,总共只有三个关键函数:
create(context):入口。先读取配置context.options[0]中的exceptions数组(默认空数组),随后返回一个Program节点访问器。Program(node):在遍历到程序根节点时,通过sourceCode.getScope(node)拿到全局作用域(global scope),遍历其中的每个变量。checkVariable(variable):核心筛选逻辑——只有满足variable.writeable === false(即该变量是只读的)且名字不在exceptions白名单中的变量才会被进一步检查;随后对其全部引用执行checkReference。checkReference(reference, index, references):对每个引用,若满足reference.init === false(不是变量声明初始化)且reference.isWrite()(是一个写引用),则报告错误nativeReassign,消息为"Read-only global '{{name}}' should not be modified."。
需要注意checkReference中有一个处理解构赋值的细节:解构赋值可能为同一个标识符产生多个写引用,因此通过index === 0 || references[index - 1].identifier !== identifier去重,避免对同一标识符重复报告。
这套机制意味着:规则并不需要硬编码"内置对象清单",而是完全依赖 scope manager 对变量writeable状态的判定——无论该状态来自语言内置全局、/*global*/配置注释,还是配置文件中的globals设置,规则都能统一覆盖。这正是它"自动识别成百上千个内置全局"的能力来源。
测试用例印证
在 tests/lib/rules/no-native-reassign.js 中可以看到规则的完整行为边界:
- 变量声明不会被误报:
"string = 'hello world';"、"var string;"是合法代码——因为普通变量的writeable不为false; - 内置对象赋值报错:
"String = 'hello world';"、"Array = 1;"均产生nativeReassign错误; - 自增/自减也是写操作:
"String++;"同样报错,印证isWrite()会把++/--视为写引用; - 解构赋值默认值会被检测:
"({Object = 0, String = 0} = {});"(ES6)产生两个错误; - 只读全局的任意赋值形式都报错:
/*global b*/ b = 1;、function f() { b = 1; }(配合globals: { b: false })都会被拦截。
错误示例
以下代码会触发no-native-reassign报告。错误(对内置全局赋值):
/*eslint no-native-reassign: "error"*/ Object = null undefined = 1错误(对显式声明为只读的全局变量赋值):
/*eslint no-native-reassign: "error"*/ /*global window:readonly*/ window = {}通过/*global window:readonly*/配置注释,window被标记为只读,对其赋值同样被拦截。
正确示例
正确(对普通变量或自定义全局赋值不受影响):
/*eslint no-native-reassign: "error"*/ a = 1 var b = 1 b = 2正确(声明为可写(writable)的全局变量允许赋值):
/*eslint no-native-reassign: "error"*/ /*global onload:writable*/ onload = function() {}选项:exceptions 白名单
规则接受exceptions选项,用于指定一组"允许被重新赋值"的内置对象名:
{ "rules": { "no-native-reassign": ["error", {"exceptions": ["Object"]}] } }配置后,Object = 0;不再报错(tests/lib/rules/no-native-reassign.js 中的合法用例正是{ code: "Object = 0;", options: [{ exceptions: ["Object"] }] })。从源码 lib/rules/no-native-reassign.js 的 schema 定义可见:exceptions必须是字符串数组,且uniqueItems: true(不允许重复项),并且additionalProperties: false——也就是说,除了exceptions之外不接受任何其他配置属性。
与全局变量配置体系的配合
规则检测能力依赖 ESLint 的全局变量声明机制,完整的声明方式见 docs/src/use/configure/language-options.md 的 "Specify Globals" 一节,主要有两种途径:
方式一:源码内配置注释
/* global var1, var2 */ // 声明两个全局变量 /* global var1:writable, var2:writable */ // 声明并允许写入其中:readonly标记只读,:writable标记可写。基于历史原因,布尔值false与字符串"readable"等价于"readonly",布尔值true与字符串"writeable"等价于"writable",但这些旧写法已不推荐使用。
方式二:配置文件(flat config)
// eslint.config.js import { defineConfig } from "eslint/config"; export default defineConfig([ { languageOptions: { globals: { var1: "writable", var2: "readonly", }, }, }, ]);这样var1允许被覆盖,var2则被禁止覆盖,配合no-native-reassign即可对自定义只读全局形成保护。另外,languageOptions.globals支持设置为"off"来禁用某个全局(例如Promise: "off")。对于浏览器、Node 等环境的预置全局集合,可以借助globals包(如globals.browser)按环境批量启用,测试用例中onload = 0(配合globals.browser)合法、top = 0(同样配合globals.browser)非法的对照,正是环境全局集与只读状态协同工作的直接体现。
与相关规则的协作
- no-undef:
no-native-reassign历史上曾作为no-undef的一部分被拆分出来(见 docs/src/use/migrating-to-3.0.0.md)。no-undef负责报告未定义变量的使用,而no-native-reassign负责报告对只读全局的覆写,两者职责互补;no-undef文档明确建议"若同时想禁止这类赋值,参见 no-global-assign"。 - no-extend-native:禁止扩展原生对象(如给
Object.prototype添加属性),与本规则分别从"改引用"与"改原型"两个角度保护内置对象。 - no-redeclare与no-shadow:分别防止重复声明变量与变量遮蔽,与只读全局保护共同构成全局命名空间的多重防线。
规则已弃用:迁移到 no-global-assign
重要提示:no-native-reassign从 ESLint v3.3.0 起已被标记为弃用(deprecated),理由是"规则更名"(Renamed rule),官方替换规则为no-global-assign,并计划在 v11.0.0 移除(见 docs/src/_data/rules_meta.json 与 docs/src/_data/rules.json 中的deprecated元数据)。在新项目中应直接使用no-global-assign。
对比两份实现 lib/rules/no-native-reassign.js 与 lib/rules/no-global-assign.js,可以发现两者逻辑几乎完全一致(同样的Program→checkVariable→checkReference结构),区别仅在于:
- 规则 ID 与消息 ID 不同(
nativeReassignvsglobalShouldNotBeModified); no-global-assign使用defaultOptions: [{ exceptions: [] }]声明默认选项,代码更简洁;no-global-assign被标记为recommended: true(推荐开启),而no-native-reassign为recommended: false且frozen: true标记不再演进。
迁移时只需把配置中的规则名替换并保持相同的exceptions配置即可:
{ "rules": { "no-global-assign": ["error", {"exceptions": ["Object"]}] } }对应规则文档见 docs/src/rules/no-global-assign.md,其测试集 tests/lib/rules/no-global-assign.js 覆盖了与旧规则完全一致的合法/非法用例(包括解构默认值、自增自减、CommonJS 下的require、配置注释与languageOptions.globals两种声明方式等场景)。
何时不使用此规则
如果你正在有意识地覆盖某个原生对象(例如在 polyfill 或测试桩(mock)场景中替换Object、Array等内置对象),则应关闭该规则,或通过exceptions选项显式放行这些名字。此时建议在配置中显式注明原因,避免后续维护者误解。
小结
no-native-reassign是 ESLint 保护运行时全局命名空间的经典规则:它不维护硬编码清单,而是借助 scope manager 对全局变量writeable状态的跟踪,配合isWrite()写引用检测,精准拦截对只读全局的一切修改形式(赋值、自增自减、解构默认值)。理解它的实现与配置,也就理解了其继任者no-global-assign的底层机制——在迁移到后者后,同样可以利用exceptions、/*global*/注释与languageOptions.globals构建完整的全局变量防护体系。
【免费下载链接】eslintFind and fix problems in your JavaScript code.项目地址: https://gitcode.com/GitHub_Trending/es/eslint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考