# 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. ありがちな事故パターン
ありがちな事故パターン
最近の典型的な流れはこんな感じです。
- 既存のクラスに、そこそこ複雑な
handleChangeがある - 若手が生成AIに「この
handleChangeに A と B の条件を追加して」と投げる - 生成AIは「既存メソッドの編集」ではなく、新しい
handleChangeをクラス末尾に追加したコードを返す - 若手は「動いてるしコンパイルも通るからOK」と思って、そのまま貼る
- 元の
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