ESLint no-func-assign 规则详解:禁止重赋值函数声明
2026/9/12 7:01:53 网站建设 项目流程

ESLint no-func-assign 规则详解:禁止重赋值函数声明

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

no-func-assign 是 ESLint 中一条用于排查「函数被重新赋值」问题的核心规则。本指南以 规则文档 为主体,结合规则源码 lib/rules/no-func-assign.js、工具函数 lib/rules/utils/ast-utils.js 与 测试用例,全面讲解该规则的触发场景、豁免情况、底层实现原理与使用方式。读完本文,你将能够准确判断哪些代码会被该规则报告,理解函数声明提升与作用域遮蔽的边界行为,并知道如何在项目中开启和配置这条规则。

为什么需要禁止重赋值函数声明

JavaScript 中的函数有两种常见书写方式:

  • 函数声明(FunctionDeclaration)function foo() { ... }
  • 函数表达式(FunctionExpression)const foo = function() { ... };

虽然 JavaScript 解释器可能在语法层面容忍以下写法,但对一个以函数声明形式定义的函数进行覆盖或重新赋值,往往是编码失误或潜在问题的信号:

function foo() {} foo = bar;

在浏览器与 Node.js 等运行时中,函数声明会被提升(hoisting)并绑定到当前作用域;在严格模式或部分实现下,对这类函数标识符的写操作甚至可能直接抛错。更重要的是,这种写法会让代码意图混乱——foo究竟是函数还是普通变量,后续读者将难以判断。no-func-assign规则正是为了在静态分析阶段拦截这种「函数名被当作普通变量重写」的反模式。

Rule Details:规则究竟检查什么

该规则的官方定位是:禁止重新赋值函数声明(Disallow reassigningfunctiondeclarations),其 meta 中声明的规则类型为problem,即用于标记潜在的错误代码。从 源码元信息 可以看到:

meta: { type: "problem", docs: { description: "Disallow reassigning `function` declarations", recommended: true, }, schema: [], messages: { isAFunction: "'{{name}}' is a function.", }, },

关键信息有三点:

  1. type: "problem":该规则报告的是「可能出错的代码」,而非单纯的风格问题;
  2. recommended: true:说明该规则属于推荐开启的规则范畴,通常由eslint:recommended预设统一启用;
  3. schema: []:规则不接受任何配置选项。

当触发时,规则会报告消息'{{name}}' is a function.,其中{{name}}是被重赋值的函数名。

不正确的代码示例

基础场景:对函数声明直接赋值

/*eslint no-func-assign: "error"*/ function foo() {} foo = bar; function baz() { baz = bar; } let a = function hello() { hello = 123; };

以上三种情况都会被报告:

  • foo = bar;在外部直接覆盖了函数声明foo
  • 在函数baz内部对其自身标识符baz赋值(递归或反射式的重写意图);
  • 命名函数表达式(Named Function Expression)function hello() {}内部对自身名字hello赋值。

与 JSHint 的差异:赋值发生在声明之前同样被报告

no-func-assign与 JSHint 中对应检查的一个显著差异在于:由于函数声明会被提升,即使赋值语句写在函数声明之前,该标识符在赋值发生时已经是函数绑定,重赋值同样是错误。因此以下代码同样属于不正确用法:

/*eslint no-func-assign: "error"*/ foo = bar; function foo() {}

在 JSHint 的同类检查中,这种「先赋值、后声明」的写法可能被放过,而 ESLint 基于作用域分析会稳定地报告它。测试用例中对应项为:

code: "foo = bar; function foo() { };",

正确的代码示例(不会被报告)

/*eslint no-func-assign: "error"*/ let foo = function () {} foo = bar; function baz(baz) { // `baz` is shadowed. baz = bar; } function qux() { const qux = bar; // `qux` is shadowed. }

三种豁免场景的共同点是作用域遮蔽(shadowing)

  • let foo = function () {}声明的是变量foo,其值为函数表达式。重赋值变量本身合法,因为foo在这里是变量而非函数声明;
  • function baz(baz)中,形参baz遮蔽了函数名baz,内部baz = bar改写的是参数而非函数声明本身;
  • function qux() { const qux = bar; }中,块级变量qux遮蔽了外部函数qux

判断逻辑很简单:只要被赋值的标识符最终解析到「函数声明名」本身,就触发报告;若解析到的是被遮蔽后的新绑定(参数、局部变量等),则放行。

Options:无配置项

该规则没有提供任何选项,即配置时只能指定开关级别:

/*eslint no-func-assign: "error"*/ /*eslint no-func-assign: "warn"*/ /*eslint no-func-assign: "off"*/

或在配置文件中写入:

{ "rules": { "no-func-assign": "error" } }

由于规则不支持自定义参数,无需像其他规则那样传入对象形式的选项;同时建议通过eslint:recommended预设默认开启(对应其recommended: true声明),无需手动逐个配置。

源码级原理:规则是如何工作的

从 规则实现 看,规则的执行依赖 ESLint 作用域分析(scope analysis),整体分为三层:

create(context) { const sourceCode = context.sourceCode; function checkReference(references) { astUtils.getModifyingReferences(references).forEach(reference => { context.report({ node: reference.identifier, messageId: "isAFunction", data: { name: reference.identifier.name }, }); }); } function checkVariable(variable) { if (variable.defs[0].type === "FunctionName") { checkReference(variable.references); } } function checkForFunction(node) { sourceCode.getDeclaredVariables(node).forEach(checkVariable); } return { FunctionDeclaration: checkForFunction, FunctionExpression: checkForFunction, }; },

工作流程如下:

  1. 收集声明变量:通过sourceCode.getDeclaredVariables(node)拿到函数节点内声明的全部变量,然后逐个调用checkVariable
  2. 筛选函数名变量checkVariable检查variable.defs[0].type是否为FunctionName——只有以「函数名」形式定义的变量(即函数声明或命名函数表达式)才会进入下一步检查,普通变量直接跳过;
  3. 筛选写引用checkReference调用 getModifyingReferences 过滤出「非初始化且可写」的引用,并对每个引用在对应标识符节点上报告错误。

因此,规则本质上是:遍历所有函数(声明与表达式),找出其「函数名」变量上发生的修改型引用(modifying reference)

getModifyingReferences 的过滤逻辑

核心过滤函数为 isModifyingReference:

function isModifyingReference(reference, index, references) { const identifier = reference.identifier; const modifyingDifferentIdentifier = index === 0 || references[index - 1].identifier !== identifier; return ( identifier && reference.init === false && reference.isWrite() && modifyingDifferentIdentifier ); }

它同时满足四个条件才算「修改型引用」:

  • identifier存在;
  • reference.init === false:排除变量声明时的初始化引用(let foo = ...的右侧初始化不算「修改」);
  • reference.isWrite():该引用是写操作(赋值、++、解构赋值等);
  • modifyingDifferentIdentifier:去重——因为解构赋值可能对同一标识符产生多个默认值引用(({x: foo = 0} = bar)foo可能同时存在多次写引用),只保留首个,避免同一位置重复报告。

这正是测试用例中解构赋值场景能够被正确识别的原理,例如:

"[foo] = bar; function foo() { };" "({x: foo = 0} = bar); function foo() { };"

这两段代码都会命中解构写引用并被报告(见 tests/lib/rules/no-func-assign.js)。

对命名函数表达式与 IIFE 的处理

由于FunctionExpression也被注册为监听节点,命名函数表达式内部的自我赋值会被捕获,例如var a = function foo() { foo = 123; };会被报告。同时,即便函数表达式被包裹在 IIFE 中,只要函数名变量发生写引用,依旧会被识别(测试中的(function() { ({x: foo = 0} = bar); function foo() { }; })();即验证了嵌套场景)。

测试用例验证与行为边界

完整的有效/无效用例位于 tests/lib/rules/no-func-assign.js,可作为理解规则边界的权威参考。

无效(invalid)用例汇总:

场景示例代码
外部直接赋值function foo() {}; foo = bar;
函数体内自我赋值function foo() { foo = bar; }
赋值先于声明(提升场景)foo = bar; function foo() { };
数组解构赋值[foo] = bar; function foo() { };
对象解构默认值赋值({x: foo = 0} = bar); function foo() { };
函数体内解构赋值function foo() { [foo] = bar; }
IIFE 内嵌套(function() { ({x: foo = 0} = bar); function foo() { }; })();
命名函数表达式自我赋值var a = function foo() { foo = 123; };

有效(valid)用例汇总:

场景示例代码
函数体内同名局部变量function foo() { var foo = bar; }
形参遮蔽函数名function foo(foo) { foo = bar; }
先声明后赋值的局部变量function foo() { var foo; foo = bar; }
箭头函数赋给变量后重赋值var foo = () => {}; foo = bar;
函数表达式赋给变量后重赋值var foo = function() {}; foo = bar;
函数表达式内部赋值(变量名)var foo = function() { foo = bar; };
模块中局部遮蔽import bar from 'bar'; function foo() { var foo = bar; }

所有无效用例均断言messageId: "isAFunction"data.name为对应函数名,与规则实现中 messages.isAFunction 的定义完全一致。如果你想在自己的项目中复现这些行为,可通过 RuleTester 编写单元测试来验证规则输出。

实践建议

  • 优先启用eslint:recommended:该规则被标记为recommended: true,推荐在项目配置中直接继承eslint:recommended,让 ESLint 自动以error级别启用它;
  • 不要把「重写函数」当作合法技巧:即使代码当前能运行,no-func-assign也会将其标记为潜在问题;如确需「可替换的函数」语义,应使用letconst声明的函数表达式,而非改写函数声明;
  • 利用遮蔽特性规避误报:当函数名在内部被形参或局部变量合法遮蔽时,规则不会误报,因此无需添加eslint-disable注释;
  • 结合 JSHint 迁移场景:从 JSHint 迁移到 ESLint 时,注意「赋值先于函数声明」的代码在 ESLint 中会被报告,需要按正确语义重排代码或改用函数表达式。

总结

no-func-assign是一条零配置、开箱即用的 problem 型规则:它基于 ESLint 的作用域分析,精准拦截对函数声明名与命名函数表达式名的重赋值,同时通过「非初始化写引用」过滤与作用域遮蔽识别,避免对合法代码产生误报。理解其源码实现与测试边界,能帮助你在实际项目中既避免「覆盖函数」这类隐蔽错误,又能在必要时正确书写被遮蔽的同名变量。

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

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

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

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

立即咨询