ESLint 的 class-methods-use-this 规则完全指南:强制类实例方法使用 `this`,识别可重构的“伪方法“
2026/9/11 16:50:49 网站建设 项目流程

ESLint 的 class-methods-use-this 规则完全指南:强制类实例方法使用this,识别可重构的"伪方法"

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

导读

class-methods-use-this是 ESLint 内置的一条suggestion类规则,它用于检测类中没有使用this的实例方法,帮助开发者识别那些本不需要作为实例方法存在、可以安全重构为普通函数或静态方法的"伪方法",同时也能捕获开发者忘记使用实例数据的情况。本篇以仓库中的规则文档 docs/src/rules/class-methods-use-this.md 为骨架,结合规则实现源码 lib/rules/class-methods-use-this.js 与测试用例 tests/lib/rules/class-methods-use-this.js,完整讲解规则的设计动机、判定原理、四个配置选项(exceptMethodsenforceForClassFieldsignoreOverrideMethodsignoreClassesWithImplements)的用法,以及何时应当关闭本规则。


一、设计动机:实例方法是一种 API 契约

在 JavaScript 中,类常被用来把可复用的逻辑——尤其是有状态的逻辑——封装进一个对象,实例的状态通过this访问。当一个 API 以实例方法的形式对外暴露时,它向调用者传递了两层信号:

  1. 方法的结果与调用它的对象相关,包括可能与该对象的状态相关。同一个方法作用于不同对象会得到不同结果:
const array1 = [1, 2, 3]; const array2 = [4, 5, 6]; // 在不同对象上调用 includes() 得到不同结果: array1.includes(1); // true array2.includes(1); // false // 修改对象状态可能改变其实例方法的结果: array2.push(1); array2.includes(1); // true
  1. 方法脱离关联对象就无法理解。例如,没有数组可操作时,Array#includes()就没有意义。

然而,类中完全可以存在一个不使用this的方法:

class Person { sayHi() { console.log("Hi!"); } } const person = new Person(); person.sayHi(); // => "Hi!"

如果某个类实例方法不使用this,通常意味着它不访问任何实例状态,因此本质上不需要作为方法存在。它有时候可以安全地重构为普通函数或静态方法,从而更准确地向 API 使用者传达意图。以sayHi为例:

// 普通函数 function sayHi() { console.log("Hi!"); } // 不再需要 Person 类或其任何实例 sayHi(); // => "Hi!" // 或者,如果静态方法能提供更自然的 API,也可以改造成静态方法 class Person { static sayHi() { console.log("Hi!"); } } Person.sayHi(); // => "Hi!" // 请注意,无论哪种改法,下面这段代码现在都会抛错, // 因为 sayHi() 已经不再是实例方法! // // const person = new Person(); // person.sayHi();

除此之外,还有一种常见情况:作者可能忘记使用本想包含的实例数据。比如构造函数里保存了this.name,方法里却忘了读取它:

class Person { constructor(name) { this.name = name; } sayHi() { console.log(`Hi from ${this.name}!`); } } const alice = new Person('Alice'); alice.sayHi(); // => 'Hi from Alice!' const bob = new Person('Bob'); bob.sayHi(); // => 'Hi from Bob!'

规则文档明确区分了这两种情形:前者(方法确实不需要this)提示你可以重构 API 形态;后者(方法遗漏了实例数据)则提示你可能存在逻辑缺陷。class-methods-use-this正是在这两类问题上同时给出信号。


二、规则行为(Rule Details)

该规则会标记不使用this的类实例方法

不正确的代码示例:

/*eslint class-methods-use-this: "error"*/ class A { foo() { console.log("Hello World"); /* error Expected 'this' to be used by class method 'foo'. */ } }

正确的代码示例:

/*eslint class-methods-use-this: "error"*/ class A { foo() { this.bar = "Hello World"; // OK,使用了 this } } class B { constructor() { // OK,constructor 被豁免 } } class C { static foo() { // OK,静态方法本就不要求使用 this } static { // OK,静态块被豁免 } }

从正确示例可以看出规则内置的三类豁免:

  • 构造函数(constructor)kind === "constructor"的方法不参与检查(见源码isInstanceMethod判断:!node.static && node.kind !== "constructor",lib/rules/class-methods-use-this.js#L109-L119);
  • 静态方法node.static为真的成员一律跳过;
  • 静态块(static block):不要求使用this

三、源码级实现原理

规则实现整体位于 lib/rules/class-methods-use-this.js,核心思路可以概括为:用栈跟踪"当前函数是否使用过this",在函数退出时对属于实例方法的函数体做检查

1. 用栈跟踪this的使用

规则在create(context)中维护一个stack数组(lib/rules/class-methods-use-this.js#L75):

  • pushContext()向栈顶压入false(进入一个函数作用域);
  • popContext()弹出栈顶标志;
  • markThisUsed()把栈顶标志置为true(lib/rules/class-methods-use-this.js#L207-L211)。

监听器通过以下 AST 节点驱动(lib/rules/class-methods-use-this.js#L213-L248):

  • 进入/退出FunctionDeclarationFunctionExpression时压栈/出栈;
  • ThisExpressionSuper节点出现时调用markThisUsed标记当前上下文已使用this
  • 类字段的值被视为"隐式函数":AccessorProperty > *.key:exit压栈、AccessorProperty:exit出栈,PropertyDefinition同理;
  • 静态块同样是隐式函数,需要单独压栈/出栈。注释给出了关键原因:静态块拥有自己的this,其中的this不应算作外围上下文已使用this(lib/rules/class-methods-use-this.js#L227-L234)。

当退出一个函数时,exitFunction取出栈顶标志methodUsesThis,若该函数是"未被配置排除的实例方法"(isIncludedInstanceMethod)且methodUsesThis为假,则上报missingThis消息(lib/rules/class-methods-use-this.js#L187-L200)。

function exitFunction(node) { const methodUsesThis = popContext(); if (isIncludedInstanceMethod(node.parent) && !methodUsesThis) { context.report({ node, loc: astUtils.getFunctionHeadLoc(node, context.sourceCode), messageId: "missingThis", data: { name: astUtils.getFunctionNameWithKind(node), }, }); } }

2. 判定"实例方法"的标准

isInstanceMethod(lib/rules/class-methods-use-this.js#L109-L119)按节点类型区分:

function isInstanceMethod(node) { switch (node.type) { case "MethodDefinition": return !node.static && node.kind !== "constructor"; case "AccessorProperty": case "PropertyDefinition": return !node.static && enforceForClassFields; default: return false; } }
  • MethodDefinition:常规方法、getter/setter、生成器方法等,只要不是静态方法且不是构造函数,即为实例方法;
  • AccessorPropertyaccessor自动访问器字段)与PropertyDefinition(类字段):只有在enforceForClassFieldstrue时才会被当作实例成员检查。

3. 名称匹配与 exceptMethods

isIncludedInstanceMethod(lib/rules/class-methods-use-this.js#L142-L177)在isInstanceMethod为真的基础上,还处理了配置过滤逻辑,其中名称提取的细节值得注意:

  • 私有方法(node.key.type === "PrivateIdentifier")会在名称前拼接#,因此exceptMethods中需要写"#bar"才能匹配私有方法;
  • 字符串字面量方法名(如"foo"())通过astUtils.getStaticStringValue取值,因此exceptMethods: ["foo"]可以匹配"foo"()
  • 数字字面量方法名(如42())会转换为字符串"42"
  • 计算属性名(computed key)会直接返回true(纳入检查)且无法通过名称匹配豁免——因为其名称在静态分析阶段无法确定,所以exceptMethods[foo]()这类方法不生效(这一点在测试class A { [foo]() {} }配合exceptMethods: ["foo"]仍报错的用例中得到印证,见 tests/lib/rules/class-methods-use-this.js#L230-L242)。

4. 测试覆盖印证

测试文件 tests/lib/rules/class-methods-use-this.js 使用RuleTester对规则进行了覆盖,JS 与 TypeScript 各有一套用例(TypeScript 部分使用@typescript-eslint/parser,见 tests/lib/rules/class-methods-use-this.js#L436-L440)。一些值得一提的边界行为:

  • 嵌套普通函数中的this不计入外层方法(class A { foo() {var a = function () {this};} }仍报错),因为每个FunctionExpression都独立压栈(tests/lib/rules/class-methods-use-this.js#L145);
  • 而方法内箭头函数中的this会算作方法使用过thisclass A { foo() { () => this; } }通过,因为箭头函数词法绑定this),见 tests/lib/rules/class-methods-use-this.js#L53;
  • super调用同样视为使用thisclass A extends B { foo() {super.foo();} }通过),因为isInstanceMethod判定时Super: markThisUsed也会触发,见 tests/lib/rules/class-methods-use-this.js#L40;
  • 对象字面量方法(({ a(){} });)不受影响(非类成员),见 tests/lib/rules/class-methods-use-this.js#L51。

四、四个配置选项详解

规则支持一个对象类型的选项,共四个字段(默认值与 schema 定义见 lib/rules/class-methods-use-this.js#L23-L60):

选项类型默认值作用
exceptMethodsstring[][]允许指定名称的方法被本规则忽略
enforceForClassFieldsbooleantrue强制检查用作实例字段初始化器的箭头函数与函数表达式是否使用this,同样适用于accessor自动访问器字段
ignoreOverrideMethodsbooleanfalse忽略带override修饰符的成员(仅 TypeScript,需@typescript-eslint/parser
ignoreClassesWithImplements"all" \| "public-fields"未设置忽略实现了接口的类中的成员(仅 TypeScript)

1. exceptMethods

"class-methods-use-this": [<enabled>, { "exceptMethods": [<...exceptions>] }]

"exceptMethods"允许传入一个方法名数组,对这些方法忽略警告。典型场景是:外部库的规范要求你必须以普通实例方法(而非静态方法)的形式覆写某个方法,且方法体内不使用this,此时可以把该方法加入白名单。

不使用exceptMethods时的不正确示例:

/*eslint class-methods-use-this: "error"*/ class A { foo() { } }

使用exceptMethods后的正确示例(注意私有方法需要带#前缀):

/*eslint class-methods-use-this: ["error", { "exceptMethods": ["foo", "#bar"] }] */ class A { foo() { } #bar() { } }

2. enforceForClassFields

"class-methods-use-this": [<enabled>, { "enforceForClassFields": true | false }]

该选项强制要求用作实例字段初始化器的箭头函数和函数表达式使用this,同样适用于accessor关键字声明的自动访问器字段(后者属于 decorators 提案的 Stage 3 内容)。默认值为true

{ "enforceForClassFields": true }(默认)下的不正确示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": true }] */ class A { foo = () => {} }

正确示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": true }] */ class A { foo = () => {this;} }

{ "enforceForClassFields": false }下的正确示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": false }] */ class A { foo = () => {} }

TypeScript 中同样生效,accessor字段与普通字段行为一致。{ "enforceForClassFields": true }(默认)下的不正确TypeScript 示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": true }] */ class A { foo = () => {} accessor bar = () => {} }

正确示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": true }] */ class A { foo = () => {this;} accessor bar = () => {this;} }

{ "enforceForClassFields": false }下的正确 TypeScript 示例:

/*eslint class-methods-use-this: ["error", { "enforceForClassFields": false }] */ class A { foo = () => {} accessor bar = () => {} }

实现层面,当enforceForClassFieldstrue时,规则会额外注册针对类字段值中箭头函数的监听器(lib/rules/class-methods-use-this.js#L238-L247):

...(enforceForClassFields && { "AccessorProperty > ArrowFunctionExpression.value": enterFunction, "AccessorProperty > ArrowFunctionExpression.value:exit": exitFunction, "PropertyDefinition > ArrowFunctionExpression.value": enterFunction, "PropertyDefinition > ArrowFunctionExpression.value:exit": exitFunction, }),

也就是说,字段初始化的箭头函数被当作独立函数上下文压栈;函数表达式(foo = function() {})则经由通用的FunctionExpression监听器进入同样的检查流程。静态字段(static foo = () => {})因isInstanceMethod!node.static条件为假而豁免——这一行为在测试 tests/lib/rules/class-methods-use-this.js#L88-L94 中有明确覆盖。

3. ignoreOverrideMethods

"class-methods-use-this": [<enabled>, { "ignoreOverrideMethods": true | false }]

该选项忽略带override修饰符的成员。默认值为false仅 TypeScript 生效(需要@typescript-eslint/parser)。典型场景:子类覆写基类抽象成员时,函数体可能为空或仅做标记,强制其使用this反而会阻碍正常的覆写模式。

{ "ignoreOverrideMethods": false }(默认)下的不正确 TypeScript 示例:

/*eslint class-methods-use-this: ["error", { "ignoreOverrideMethods": false }] */ abstract class Base { abstract method(): void; abstract property: () => void; } class Derived extends Base { override method() {} override property = () => {}; }

默认选项下的正确 TypeScript 示例(使用了this):

/*eslint class-methods-use-this: ["error", { "ignoreOverrideMethods": false }] */ abstract class Base { abstract method(): void; abstract property: () => void; } class Derived extends Base { override method() { this.foo = "Hello World"; }; override property = () => { this; }; }

{ "ignoreOverrideMethods": true }下的正确 TypeScript 示例:

/*eslint class-methods-use-this: ["error", { "ignoreOverrideMethods": true }] */ abstract class Base { abstract method(): void; abstract property: () => void; } class Derived extends Base { override method() {} override property = () => {}; }

实现上,isIncludedInstanceMethod中首先检查if (ignoreOverrideMethods && node.override) return false;(lib/rules/class-methods-use-this.js#L144-L146)。测试覆盖了override与各种 TS 修饰符组合:private overrideprotected overrideoverride accessoroverride get getter()override set setter()等(见 tests/lib/rules/class-methods-use-this.js#L503-L666)。

4. ignoreClassesWithImplements

"class-methods-use-this": [<enabled>, { "ignoreClassesWithImplements": "all" | "public-fields" }]

该选项忽略实现了接口的类中定义的成员,仅 TypeScript 生效。接受两个值:

  • "all"—— 忽略所有实现了接口的类中的成员;
  • "public-fields"—— 只忽略实现了接口的类中的公有字段privateprotected成员仍参与检查)。

设计初衷是:类在实现接口时,接口往往只约束成员的存在与签名,并不要求成员访问实例状态,空实现(只声明、不使用this)是常见的合法写法。

{ "ignoreClassesWithImplements": "all" }下的不正确 TypeScript 示例(未实现接口的普通类仍会被检查):

/*eslint class-methods-use-this: ["error", { "ignoreClassesWithImplements": "all" }] */ class Standalone { method() {} property = () => {}; }

正确 TypeScript 示例(实现了接口的类被整体豁免):

/*eslint class-methods-use-this: ["error", { "ignoreClassesWithImplements": "all" }] */ interface Base { method(): void; } class Derived implements Base { method() {} property = () => {}; }

{ "ignoreClassesWithImplements": "public-fields" }下的不正确 TypeScript 示例(private/protected成员不受豁免):

/*eslint class-methods-use-this: ["error", { "ignoreClassesWithImplements": "public-fields" }] */ interface Base { method(): void; } class Derived implements Base { method() {} property = () => {}; private privateMethod() {} private privateProperty = () => {}; protected protectedMethod() {} protected protectedProperty = () => {}; }

正确 TypeScript 示例(仅公有字段被豁免,私有/受保护成员未出现,故无报错):

/*eslint class-methods-use-this: ["error", { "ignoreClassesWithImplements": "public-fields" }] */ interface Base { method(): void; } class Derived implements Base { method() {} property = () => {}; }

实现上,hasImplements向上查找node.parent.parent,确认外层是ClassDeclarationClassExpressionclassNode.implements?.length > 0(lib/rules/class-methods-use-this.js#L127-L134)。而"public-fields"的细粒度过滤条件为:node.key.type !== "PrivateIdentifier"(排除私有字段)且(!node.accessibility || node.accessibility === "public")(排除private/protected),见 lib/rules/class-methods-use-this.js#L148-L161。需要注意:私有方法/私有字段即使在"public-fields"下也不会被豁免,测试中有大量此类边界用例(如 tests/lib/rules/class-methods-use-this.js#L1090-L1106)。


五、在配置文件中启用规则

该规则默认不开启recommended: false,见 lib/rules/class-methods-use-this.js#L34),属于suggestion类型(meta.type: "suggestion",见 lib/rules/class-methods-use-this.js#L21)。它支持 JavaScript 与 TypeScript 两种方言(dialects: ["JavaScript", "TypeScript"],见 lib/rules/class-methods-use-this.js#L33)。

在 ESLint 的 flat config(eslint.config.js)中启用并配置的示例:

export default [ { rules: { "class-methods-use-this": [ "error", { exceptMethods: [], // 默认:不豁免任何方法名 enforceForClassFields: true, // 默认:检查类字段初始化的箭头函数/函数表达式 ignoreOverrideMethods: false, // 默认:不忽略 override 成员(TS) // ignoreClassesWithImplements: "all", // TS:忽略实现接口的类 }, ], }, }, ];

也可以在旧式eslintrc配置(.eslintrc.*)中写成:

{ "rules": { "class-methods-use-this": ["warn", { "exceptMethods": ["render"] }] } }

需要提醒的是:后三个选项中的ignoreOverrideMethodsignoreClassesWithImplements依赖 TypeScript 语法(override修饰符、implements子句),必须配合@typescript-eslint/parser使用;如果目标文件是纯 JavaScript,这两个选项不会产生实际作用。


六、何时不应使用此规则(When Not To Use It)

修复本规则的违规几乎总是破坏性变更(breaking change),因为需要在受影响方法的每一个调用点做出改动。因此,如果满足以下任一条件,很可能不适合处理本规则的违规:

  • 你的项目有下游消费者且不能破坏
  • 你不希望对所有方法调用点做侵入式修改。

例如,一个被广泛引用的类库,其公共实例方法若改为静态方法或普通函数,所有使用new+ 实例调用的外部代码都会失效。此时建议关闭本规则,或在exceptMethods中列出确实需要保留为实例形态的方法。


七、小结

class-methods-use-this通过一个简洁的栈式this使用跟踪机制,把"实例方法是否真正依赖实例状态"转化为可静态检查的规则,帮助开发者:

  • 发现可以重构为普通函数或静态方法的"伪实例方法",改善 API 设计意图的表达;
  • 捕获忘记使用实例数据的逻辑缺陷;
  • 通过exceptMethodsenforceForClassFieldsignoreOverrideMethodsignoreClassesWithImplements四个选项,灵活适配外部库覆写要求、类字段初始化器、TypeScriptoverride与接口实现等真实场景。

配套实现与测试分别位于 lib/rules/class-methods-use-this.js 与 tests/lib/rules/class-methods-use-this.js,读者可对照源码与 1376 行测试用例进一步研究规则的边界行为;规则中文档(docs/src/rules/class-methods-use-this.md)则是该规则的权威使用说明。

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

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

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

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

立即咨询