
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完整讲解规则的设计动机、判定原理、四个配置选项exceptMethods、enforceForClassFields、ignoreOverrideMethods、ignoreClassesWithImplements的用法以及何时应当关闭本规则。一、设计动机实例方法是一种 API 契约在 JavaScript 中类常被用来把可复用的逻辑——尤其是有状态的逻辑——封装进一个对象实例的状态通过this访问。当一个 API 以实例方法的形式对外暴露时它向调用者传递了两层信号方法的结果与调用它的对象相关包括可能与该对象的状态相关。同一个方法作用于不同对象会得到不同结果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方法脱离关联对象就无法理解。例如没有数组可操作时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() { // OKconstructor 被豁免 } } class C { static foo() { // OK静态方法本就不要求使用 this } static { // OK静态块被豁免 } }从正确示例可以看出规则内置的三类豁免构造函数constructorkind constructor的方法不参与检查见源码isInstanceMethod判断!node.static node.kind ! constructorlib/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#L75pushContext()向栈顶压入false进入一个函数作用域popContext()弹出栈顶标志markThisUsed()把栈顶标志置为truelib/rules/class-methods-use-this.js#L207-L211。监听器通过以下 AST 节点驱动lib/rules/class-methods-use-this.js#L213-L248进入/退出FunctionDeclaration与FunctionExpression时压栈/出栈ThisExpression与Super节点出现时调用markThisUsed标记当前上下文已使用this类字段的值被视为隐式函数AccessorProperty *.key:exit压栈、AccessorProperty:exit出栈PropertyDefinition同理静态块同样是隐式函数需要单独压栈/出栈。注释给出了关键原因静态块拥有自己的this其中的this不应算作外围上下文已使用thislib/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. 判定实例方法的标准isInstanceMethodlib/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类字段只有在enforceForClassFields为true时才会被当作实例成员检查。3. 名称匹配与 exceptMethodsisIncludedInstanceMethodlib/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#L53super调用同样视为使用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/parserignoreClassesWithImplementsall \| public-fields未设置忽略实现了接口的类中的成员仅 TypeScript1. exceptMethodsclass-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. enforceForClassFieldsclass-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 () {} }实现层面当enforceForClassFields为true时规则会额外注册针对类字段值中箭头函数的监听器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. ignoreOverrideMethodsclass-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 override、protected override、override accessor、override get getter()、override set setter()等见 tests/lib/rules/class-methods-use-this.js#L503-L666。4. ignoreClassesWithImplementsclass-methods-use-this: [enabled, { ignoreClassesWithImplements: all | public-fields }]该选项忽略实现了接口的类中定义的成员仅 TypeScript 生效。接受两个值all—— 忽略所有实现了接口的类中的成员public-fields—— 只忽略实现了接口的类中的公有字段private、protected成员仍参与检查。设计初衷是类在实现接口时接口往往只约束成员的存在与签名并不要求成员访问实例状态空实现只声明、不使用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确认外层是ClassDeclaration或ClassExpression且classNode.implements?.length 0lib/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 configeslint.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] }] } }需要提醒的是后三个选项中的ignoreOverrideMethods与ignoreClassesWithImplements依赖 TypeScript 语法override修饰符、implements子句必须配合typescript-eslint/parser使用如果目标文件是纯 JavaScript这两个选项不会产生实际作用。六、何时不应使用此规则When Not To Use It修复本规则的违规几乎总是破坏性变更breaking change因为需要在受影响方法的每一个调用点做出改动。因此如果满足以下任一条件很可能不适合处理本规则的违规你的项目有下游消费者且不能破坏你不希望对所有方法调用点做侵入式修改。例如一个被广泛引用的类库其公共实例方法若改为静态方法或普通函数所有使用new 实例调用的外部代码都会失效。此时建议关闭本规则或在exceptMethods中列出确实需要保留为实例形态的方法。七、小结class-methods-use-this通过一个简洁的栈式this使用跟踪机制把实例方法是否真正依赖实例状态转化为可静态检查的规则帮助开发者发现可以重构为普通函数或静态方法的伪实例方法改善 API 设计意图的表达捕获忘记使用实例数据的逻辑缺陷通过exceptMethods、enforceForClassFields、ignoreOverrideMethods、ignoreClassesWithImplements四个选项灵活适配外部库覆写要求、类字段初始化器、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),仅供参考