🐈

# JavaScript のクラスで「同名メソッドが後勝ち」になる理由を、仕様からちゃんと押さえる

に公開

はじめに

JavaScript や TypeScript で、現場でこんな重複バグなコードを見たことがあるでしょうか。

class FormHandler {
  handleChange() {
    //なにかの処理Aの改訂版
    console.log('A’');
  }

  handleChange() {
   //なにかの処理A
    console.log('A');
  }
}

new FormHandler().handleChange();

実行結果はエラーにはならず A だけが出力されます。A’ 側は一度も呼ばれません。

一見「バグっぽい書き方」ですが、これは Chrome / Firefox など主要ブラウザで共通しており、しかも 仕様としてこういう挙動になる と定義されています。

この記事では、

  • なぜ「後勝ち」になるのか(クラス / オブジェクトの仕様)
  • それは仕様なのか、実装依存なのか
  • Lint や TypeScript でどう防ぐか
    を整理します。

1. 何が起きているのか:挙動の確認

改めて、先ほどのコードです。

class FormHandler {
  handleChange() {
    <!--なにかの処理Aの改訂版  -->
    console.log('A’');
  }

  handleChange() {
   <!--なにかの処理A  -->
    console.log('A');
  }
}

new FormHandler().handleChange();
// => "A"
  • 構文エラーにはならない
  • TypeScript でも、そのままコンパイルできるケースがある
  • 実行しても例外は出ず、「後ろに書いたメソッドだけ」が有効

つまり、知らないうちに静かに後勝ちしている状態。


2. なぜ後勝ちなのか:クラスは「プロトタイプの糖衣構文」

class 構文は、内部的には「プロトタイプにプロパティを定義する糖衣構文」です。
つまり「クラス専用の特別な入れ物」があるわけではなく、「プロトタイプという普通のオブジェクトに、メソッドを順番に登録しているだけ」**と思っておくと挙動が理解しやすいです

極端に崩して書くと、次のようなイメージになります。

class FormHandler {}

// 1つ目の handleChange
Object.defineProperty(FormHandler.prototype, 'handleChange', {
  value: function () {
    console.log('AA');
  }
});

// 2つ目の handleChange(上書き)
Object.defineProperty(FormHandler.prototype, 'handleChange', {
  value: function () {
    console.log('A');
  }
});

FormHandler.prototype.handleChange という 同じプロパティに 2 回代入しているだけなので、最後の定義で上書きされます。

この「同じ名前のプロパティは後ろが勝つ」というルールは、クラスだけでなくオブジェクトリテラルでも同じです。

const obj = { x: 1, x: 2 };
console.log(obj.x); // 2

MDN の Object initializer のページでも、

同じ名前のプロパティを使った場合、2 つ目のプロパティが 1 つ目を上書きする。 (MDN Web Docs)

と明記されています。

クラスについても、MDN の Classes の項目にこう書かれています。

public プロパティは同じ名前を複数回定義でき、その場合は最後のものが他を上書きする。これはオブジェクト初期化子と同じ挙動である。 (MDN Web Docs)

つまり、**クラスの同名メソッドが後勝ちになるのは、ブラウザ実装のクセではなく「仕様どおりの挙動」**です。


3. 仕様なのか? 歴史的な経緯

「JavaScript はそういうもの」という雑な説明がよく出てくる背景には、歴史的な仕様変更も関わっています。

  • ECMAScript 5 の strict mode までは、
    オブジェクトリテラルの 重複プロパティ名は SyntaxError だった
  • ただし ES2015 で「計算されたプロパティ名([expr])」が入った結果、
    パース時点で重複かどうかを完全には判定できなくなった
  • そのため ES2015 以降は strict mode でも重複プロパティ名を許可し、後勝ち挙動に統一された (Stack Overflow)

クラスの重複メンバーも、オブジェクトと同じく「後勝ち」のルールに乗っています。
そして __proto__ のような一部の特別なプロパティ名を除き、重複自体は構文エラーになりません (MDN Web Docs)。

要するに、

以前はエラーだった
→ ES2015 以降は「重複 OK・後勝ち」に仕様変更された
→ なのに、古い情報や感覚で「JavaScript はそういう言語」とざっくり書いてる記事がある

というのが事情です。


4. ESLint / TypeScript はどう見ているか

ESLint の no-dupe-class-members

ESLint には no-dupe-class-members というルールがあります。

クラスメンバーに同じ名前の宣言がある場合、最後の宣言が他の宣言を暗黙的に上書きする。予期しない動作を引き起こす可能性があるため禁止する。 (eslint.org)

サンプルとして、公式ドキュメントにも次のような例が載っています。

/*eslint no-dupe-class-members: "error"*/

class Foo {
  bar() { console.log("hello"); }
  bar() { console.log("goodbye"); }
}

const foo = new Foo();
foo.bar(); // goodbye

まさに今回のケースそのものです。

他の Linter(Biome / oxc 等)でも、同じ理由で「重複クラスメンバーは危険」と明示されています (Oxc)。

TypeScript + @typescript-eslint/no-dupe-class-members

TypeScript でも、クラス内に同名メソッドを複数書くと JavaScript と同じ挙動になります。
ただし TypeScript は「メソッドのオーバーロード構文」があるため、ESLint の素の no-dupe-class-members だけだと誤検知になるケースがあります。

そのため @typescript-eslint では、TypeScript 向けに拡張した @typescript-eslint/no-dupe-class-members ルールを提供しています (typescript-eslint.io)。

// .eslintrc.* の例
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    "no-dupe-class-members": "off",
    "@typescript-eslint/no-dupe-class-members": "error"
  }
}

これで、

  • TypeScript の正式なオーバーロードは許可
  • それ以外の「ガチ重複」はエラー

という挙動にできます。


5. ありがちな事故パターン

ありがちな事故パターン

最近の典型的な流れはこんな感じです。

  1. 既存のクラスに、そこそこ複雑な handleChange がある
  2. 若手が生成AIに「この handleChange に A と B の条件を追加して」と投げる
  3. 生成AIは「既存メソッドの編集」ではなく、新しい handleChange をクラス末尾に追加したコードを返す
  4. 若手は「動いてるしコンパイルも通るからOK」と思って、そのまま貼る
  5. 元の handleChange は二度と呼ばれない(けど誰も気づかない)

結果として、

  • 以前満たしていた仕様が、静かに消える
  • テストが薄いと、リリース後まで気づかない
  • シニア側は「なんで動作変わってるんだ?」からデバッグが始まる

という、最悪なコスパの事故が生まれます。

「JavaScript はそういうもの」で済ませない

一部のブログでは、

  • 「JavaScript は後勝ちなので注意しましょう」
  • 「JavaScript はそういう言語なので割り切りましょう」

のような書き方をしているものもあります。

ただ、実務でやりたいのは

  • 「後勝ち仕様を理解した上で」
  • 「そもそもそういうコードをプロジェクトに入れない」

という対策です。

「そういうものだから気をつけろ」で終わらせると、結局ヒューマンエラー頼みになります。


6. チームとしてどう防ぐか

(1) Lint でそもそも書けないようにする

まずはツールで機械的に潰します。

  • JS のみ:no-dupe-class-members: "error"
  • TS を使う:@typescript-eslint/no-dupe-class-members: "error"
{
  "rules": {
    "no-dupe-class-members": "off",
    "@typescript-eslint/no-dupe-class-members": "error"
  }
}

LWC プロジェクトであれば、eslint-plugin-lwc の推奨設定を有効化しておくのも有効です (GitHub)。

(2) PR / レビューで「AI が書いた部分」を明示させる

生成AIの利用を前提にするなら、PR テンプレを少し変えた方がいいです。

例:

  • この変更の目的
  • 生成AIを使った箇所
  • 自分が完全には理解しきれていない箇所(あれば)

レビュー側は最低限、

  • クラス内でメソッド名が重複していないか
  • 既存のハンドラ名をそのまま AI に渡していないか
  • Lint のエラーを // eslint-disable で踏み潰していないか

あたりをチェックしておけば、「静かな後勝ちバグ」はかなり減らせます。

(3) 若手には「説明できないコードは PR に出さない」を徹底させたい

生成AI自体は禁止しなくても、

生成AIで書いたコードも含めて、自分の言葉で説明できないなら PR に出さない

というルールにするだけで、難易度がだいぶ変わります。

  • なぜこのクラス構造なのか
  • なぜこのメソッド名・責務の分け方なのか
  • なぜこの if / 条件分岐なのか

を説明させると、「同名メソッド 2 回書いてました」系の事故は見つけやすくなります。


7. この知識はどこで効くのか

正直、「同名メソッド後勝ち」は単体で超重要なトピック、というほどではありません。
ただし、以下のような場面では効いてきます。

  • イベントハンドラがなぜか効いていないときのデバッグ
  • 「JavaScript のクラス周りの仕様を、なんとなくではなく言語化して説明する」必要が出たとき
    「なんか動かないときに、クラス定義の重複を見る」くらいのチェックポイントを頭に入れておくだけでも、デバッグの初動がだいぶ楽になります。

まとめ

  • JavaScript / TypeScript のクラスで同じ名前のメソッドを複数定義すると、最後に書かれたものだけが有効になる

  • これは ES2015 以降の仕様に沿った挙動であり、「ブラウザ実装のクセ」ではない

  • 仕様としては合法だが、実務ではほぼバグなので、ESLint の no-dupe-class-members / @typescript-eslint/no-dupe-class-members などで そもそも書けないようにするのが現実的

  • 生成AI+若手エンジニアのクソコードの量産だと、「既存メソッドを別の同名メソッドで静かに潰す」事故が起きやすい

  • チームとしては

    • Lint で機械的に防ぐ
    • PR テンプレ・レビュー観点を AI 時代用にアップデートする
    • 「AI で書いたコードも、自分で説明できる状態で出す」を徹底する

あたりを押さえておくと、「JavaScript はそういうものだから」で消耗する回数を減らせます。


参考リンク

  • MDN: Object initializer(重複プロパティ名と後勝ち挙動) (MDN Web Docs)
  • MDN: Classes(同名 public プロパティは最後の定義が有効) (MDN Web Docs)
  • ESLint: no-dupe-class-members(重複クラスメンバー禁止ルール) (eslint.org)
  • @typescript-eslint/no-dupe-class-members(TypeScript 用拡張) (typescript-eslint.io)
  • eslint-plugin-lwc: no-dupe-class-members ルール説明 (GitHub)
  • 「ES5 strict での重複禁止 → ES2015 で仕様変更」の議論(Stack Overflow 経由) (Stack Overflow)

Discussion