JavaScriptのクラスとは、データ(フィールド)とそれを扱う処理(メソッド)をひとまとめにし、同じ形のオブジェクトを new で何個でも作れるようにする構文です。 ES2022 で # から始まる本当に外から見えない private フィールド・メソッド、static フィールド、static 初期化ブロック、#x in obj によるチェックがそろい、Node.js 22 や現在の主要ブラウザでそのまま使えます。

この記事では、まずクラス構文のすべてを1枚の早見表にまとめ、次に全機能を使った架空の在庫管理クラスを Node.js 22.22.2 で実行して出力を掲載します。さらに #x と _x と TypeScript の private の違いを実行して比較し、つまずきやすい実際のエラー文の一覧と、クラス・関数・オブジェクトリテラルのどれを選ぶかの判断基準まで解説します。

JavaScript クラス構文の早見表

やりたいこと 書き方 ポイント
クラスを定義 class Item { } 宣言より前では使えない(後述のエラー)
初期化処理 constructor(name) { this.name = name; } new Item('ペン') で呼ばれる。1クラスに1つ
public フィールド unit = '個'; インスタンスごとに作られる。Object.keys に出る
メソッド receive(qty) { ... } prototype に1つだけ作られ、全インスタンスで共有
private フィールド #stock = 0; → this.#stock クラスの中からだけ読み書きできる。事前の宣言が必須
private メソッド #record() { } → this.#record() 内部の補助処理に
getter get stock() { return this.#stock; } item.stock とプロパティのように読める
setter set stock(v) { ... } item.stock = 5 の代入時に検証できる
static フィールド static count = 0; クラスに1つ。Item.count で読む
static メソッド static create() { } Item.create()。インスタンスからは呼べない
static private static #units; クラス内部の共有データ
static 初期化ブロック static { ... } クラス定義時に1回だけ動く。複雑な static の初期化に
継承 class Food extends Item { } 親のメソッドを引き継ぐ
親の constructor を呼ぶ super(name); 子の constructor では this より先に呼ぶ
親のメソッドを呼ぶ super.toString() オーバーライドしたメソッドの中で使う
型の判定 obj instanceof Item 親クラスでも true
private を持つかの判定 #stock in obj 例外なしで true / false(ES2022)
JSON に出す項目を決める toJSON() { return {...}; } private は JSON.stringify に出ないため

対応環境: private フィールド・メソッド、static フィールド、static {}、#x in obj はすべて ES2022 の仕様で、Node.js 16.11 以降と現在の主要ブラウザで使えます。この記事の出力は Node.js 22.22.2 で実行したものです。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScriptクラス入門|private・static・継承を比較表で解説」

クラスの基本形:constructor・フィールド・メソッド

最小のクラスは「フィールドを宣言 → constructor で値を入れる → メソッドで操作する」の3つでできています。

class Task {
  title;            // public フィールド(宣言だけ。初期値は undefined)
  done = false;     // 初期値つきのフィールド

  constructor(title) {
    this.title = title;
  }

  complete() {
    this.done = true;
    return this;
  }
}

const t = new Task('備品の発注');
t.complete();
console.log(t);
// Task { title: '備品の発注', done: true }
書く場所 作られる場所 共有されるか
フィールド done = false; 各インスタンス インスタンスごとに別々
メソッド complete() {} Task.prototype 全インスタンスで1つを共有
static 付き Task 自身 クラスに1つ

フィールドは constructor の先頭で実行される代入と同じ働きをします。クラスの冒頭にフィールドを並べておくと、そのオブジェクトがどんな項目を持つか一目でわかるのが利点です。

全機能を使った完成例:在庫管理クラス

早見表のすべての機能を1つにまとめた、架空の在庫管理クラスです。 そのまま inventory.mjs として保存し、node inventory.mjs で動きます。

// 架空の在庫管理クラス(Node.js 22 で実行)
class InventoryItem {
  // static フィールド:クラス全体で1つ
  static count = 0;
  static #units;                       // static private フィールド

  // static {} ブロック:クラス定義時に1回だけ動く初期化
  static {
    InventoryItem.#units = new Set(['個', '箱', '本']);
  }

  // public フィールド(インスタンスごと)
  name;
  unit = '個';

  // private フィールド(クラスの中からだけ読める)
  #stock = 0;
  #history = [];

  constructor(name, { stock = 0, unit = '個' } = {}) {
    if (!InventoryItem.#units.has(unit)) {
      throw new RangeError(`unknown unit: ${unit}`);
    }
    this.name = name;
    this.unit = unit;
    this.#stock = stock;
    InventoryItem.count++;
  }

  // getter:プロパティのように読める
  get stock() {
    return this.#stock;
  }

  // setter:代入時に検証できる
  set stock(value) {
    if (!Number.isInteger(value) || value < 0) {
      throw new RangeError(`stock must be a non-negative integer: ${value}`);
    }
    this.#record('set', value - this.#stock);
    this.#stock = value;
  }

  // 通常のメソッド(prototype に1つだけ作られる)
  receive(qty) {
    this.#record('receive', qty);
    this.#stock += qty;
    return this; // メソッドチェーン用
  }

  ship(qty) {
    if (qty > this.#stock) {
      throw new Error(`${this.name}: 在庫不足(在庫 ${this.#stock}, 出荷 ${qty})`);
    }
    this.#record('ship', -qty);
    this.#stock -= qty;
    return this;
  }

  get history() {
    return this.#history.map((h) => ({ ...h })); // コピーを返す
  }

  // private メソッド
  #record(type, diff) {
    this.#history.push({ type, diff });
  }

  // static メソッド:インスタンスを作らずに呼ぶ
  static isItem(obj) {
    return #stock in obj;               // ブランドチェック(ES2022)
  }

  static addUnit(unit) {
    InventoryItem.#units.add(unit);
  }

  toJSON() {
    return { name: this.name, unit: this.unit, stock: this.#stock };
  }

  toString() {
    return `${this.name}: ${this.#stock}${this.unit}`;
  }
}

// 継承:extends と super
class PerishableItem extends InventoryItem {
  #expiresOn;

  constructor(name, { expiresOn, ...options } = {}) {
    super(name, options);              // this を使う前に必ず呼ぶ
    this.#expiresOn = new Date(expiresOn);
  }

  isExpired(today = new Date()) {
    return today > this.#expiresOn;
  }

  // オーバーライドして親の処理も呼ぶ
  toString() {
    return `${super.toString()}(期限 ${this.#expiresOn.toISOString().slice(0, 10)})`;
  }
}

const pen = new InventoryItem('ボールペン', { stock: 10, unit: '本' });
pen.receive(5).ship(3);
console.log(String(pen));
console.log(pen.stock);
pen.stock = 20;
console.log(pen.history);
console.log(JSON.stringify(pen));
console.log(Object.keys(pen));

const milk = new PerishableItem('牛乳', { stock: 6, unit: '本', expiresOn: '2026-10-05' });
console.log(String(milk));
console.log(milk.isExpired(new Date('2026-10-01')), milk.isExpired(new Date('2026-10-06')));
console.log(milk instanceof PerishableItem, milk instanceof InventoryItem, pen instanceof PerishableItem);
console.log(InventoryItem.isItem(milk), InventoryItem.isItem({ stock: 1 }));
console.log(InventoryItem.count, typeof pen.isItem, typeof InventoryItem.prototype.receive);

try { pen.ship(100); } catch (e) { console.log(`${e.name}: ${e.message}`); }
try { pen.stock = -1; } catch (e) { console.log(`${e.name}: ${e.message}`); }
try { new InventoryItem('机', { unit: '台' }); } catch (e) { console.log(`${e.name}: ${e.message}`); }
InventoryItem.addUnit('台');
console.log(String(new InventoryItem('机', { unit: '台', stock: 2 })), InventoryItem.count);

実行結果(Node.js 22.22.2)

ボールペン: 12本
12
[
  { type: 'receive', diff: 5 },
  { type: 'ship', diff: -3 },
  { type: 'set', diff: 8 }
]
{"name":"ボールペン","unit":"本","stock":20}
[ 'name', 'unit' ]
牛乳: 6本(期限 2026-10-05)
false true
true true false
true false
2 undefined function
Error: ボールペン: 在庫不足(在庫 20, 出荷 100)
RangeError: stock must be a non-negative integer: -1
RangeError: unknown unit: 台
机: 2台 3

出力から読み取れること

出力 わかること
ボールペン: 12本 receive(5).ship(3) は return this によるメソッドチェーン。10 + 5 − 3 = 12
[ 'name', 'unit' ] private フィールドは Object.keys に出ない
{"name":...,"stock":20} toJSON() を書いたので、private の在庫数も JSON に出せた
true true false 子クラスのインスタンスは親クラスの instanceof でも true。逆は false
true false #stock in obj は、同じ名前の public プロパティ stock を持つだけのオブジェクトには false
2 undefined function static メソッドはインスタンスからは見えない。通常メソッドは prototype にある
RangeError: unknown unit: 台 → 机: 2台 3 static private の #units を static メソッド経由で更新できた。失敗した new は count を増やしていない

getter / setter を使うと、pen.stock = 20 という自然な書き方のまま検証を挟めます。 一方で、外から書き換えさせたくない値は setter を作らず getter だけにします(代入するとエラーになります。後述)。

static と static {} ブロックの使いどころ

static は「インスタンスごとではなく、クラスに1つあればよいもの」に使います。

用途 例 備考
ファクトリ(生成用メソッド) static fromJSON(json) { return new Item(json.name, json); } constructor の引数が複雑なときに読みやすくなる
判定・変換などの補助関数 static isItem(obj) インスタンスがなくても呼べる
定数・設定 static MAX_STOCK = 9999; Item.MAX_STOCK で参照
生成数などの共有データ static count = 0; テストでは前の状態が残る点に注意
複雑な初期化 static { ... } try/catch や複数の static 間の計算をまとめられる

static {} ブロックの中では、同じクラスの private 要素(#units)に触れられます。 初期化が1行で済むなら static #units = new Set([...]); と書いても同じです。

static private と this の注意点

static メソッドの中で this.#make() と書くと、子クラスから呼んだときに this が子クラスになり、エラーになります。

class Base {
  static #make() { return 'made'; }
  static build() { return this.#make(); }       // this は呼び出し元のクラス
  static buildSafe() { return Base.#make(); }   // クラス名で呼ぶ
}
class Derived extends Base {}
console.log(Base.build(), Derived.buildSafe());
// made made
try { Derived.build(); } catch (e) { console.log(`${e.name}: ${e.message}`); }
// TypeError: Receiver must be class Base

static の private 要素は、this ではなくクラス名(Base.#make())で呼ぶと継承しても動きます。

#x と _x と TypeScript の private の比較

「外から触らせない」書き方は3通りありますが、実際に隠れるのは # だけです。 3つを実行して比べました。

class Hash { #secret = 'h'; name = 'item'; readFrom(o) { return o.#secret; } }
class Under { _secret = 'u'; name = 'item'; }
class ChildHash extends Hash { peek() { return this.secret; } }
class ChildUnder extends Under { peek() { return this._secret; } }
const h = new Hash(), u = new Under();
console.log('read  #:', h.secret, '| _:', u._secret, "| h['#secret']:", h['#secret']);
console.log('JSON  #:', JSON.stringify(h), '| _:', JSON.stringify(u));
console.log('keys  #:', Object.keys(h), '| _:', Object.keys(u));
console.log('child #:', new ChildHash().peek(), '| _:', new ChildUnder().peek());
console.log('log   #:', h, '| _:', u);
console.log('spread #:', { ...h }, '| clone:', structuredClone(h));
try { h.readFrom({}); } catch (e) { console.log(`${e.name}: ${e.message}`); }
read  #: undefined | _: u | h['#secret']: undefined
JSON  #: {"name":"item"} | _: {"_secret":"u","name":"item"}
keys  #: [ 'name' ] | _: [ '_secret', 'name' ]
child #: undefined | _: u
log   #: Hash { name: 'item' } | _: Under { _secret: 'u', name: 'item' }
spread #: { name: 'item' } | clone: { name: 'item' }
TypeError: Cannot read private member #secret from an object whose class did not declare it

TypeScript の private / protected は TypeScript 7.0.2 でコンパイルして確かめました。

class TsItem {
  private secret = 't';
  protected note = 'n';
  name = 'item';
}
class TsChild extends TsItem {
  peekNote() { return this.note; }
  peekSecret() { return this.secret; }
}
const t = new TsItem();
console.log('read:', t.secret);
console.log('bracket:', t['secret']);
console.log('JSON:', JSON.stringify(t), 'keys:', Object.keys(t));
console.log('child:', new TsChild().peekNote());
$ npx tsc --target es2022 --module nodenext --strict ts-private.ts
ts-private.ts(8,30): error TS2341: Property 'secret' is private and only accessible within class 'TsItem'.
ts-private.ts(11,24): error TS2341: Property 'secret' is private and only accessible within class 'TsItem'.

$ node ts-private.js   # エラーがあっても JS は出力される
read: t
bracket: t
JSON: {"secret":"t","note":"n","name":"item"} keys: [ 'secret', 'note', 'name' ]
child: n
観点 #secret(JS の private) _secret(命名の約束) TS の private secret
外から読めるか 読めない(undefined、.# は構文エラー) 読める コンパイルエラー。実行時は読める
obj['secret'] で読めるか 読めない 読める コンパイルも通り、読める
JSON.stringify に出るか 出ない 出る 出る
Object.keys / スプレッドに出るか 出ない 出る 出る
console.log に出るか 出ない(Node 22) 出る 出る
子クラスから使えるか 使えない(構文エラー) 使える 使えない(protected なら使える)
守られる時点 実行時(言語仕様) 守られない(読み手への合図) コンパイル時のみ
主な用途 本当に隠したい内部状態 古いコードとの互換、「内部用」の目印 型チェックで十分なとき、protected が必要なとき

判断の目安は次のとおりです。

  • 外から絶対に触らせたくない値(在庫数を setter 経由でしか変えさせない等)→ #
  • 子クラスからは使わせたい(JS には protected がない)→ TypeScript の protected、または _ の命名+ドキュメント
  • JSON.stringify にそのまま出したい → public フィールド、または # + toJSON()
  • TypeScript で書くなら、# と private は併用できます。実行時にも隠したいなら # を選びます

クラスでよく出るエラー文一覧

Node.js 22.22.2 で実際に出たエラー文です。 1行ずつ別ファイルにして実行しました。

コード エラー文 原因と直し方
クラス内で宣言せずに this.#x SyntaxError: Private field '#x' must be declared in an enclosing class #x; をクラスの先頭で宣言する。this.#x = 1 だけでは作れない
クラスの外で a.#x SyntaxError: Private field '#x' must be declared in an enclosing class 外からは読めない。getter を用意する
子クラスで親の this.#s SyntaxError: Private field '#s' must be declared in an enclosing class 親に getter かメソッドを用意し、それを呼ぶ
#x; を2回宣言 SyntaxError: Identifier '#x' has already been declared 1回にする
delete this.#x SyntaxError: Private fields can not be deleted this.#x = undefined などで値を空にする
子の constructor で super() より前に this ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor super(...) を1行目に書く
子の constructor に super() が無い 同上(ReferenceError: Must call super constructor ...) constructor を書くなら super() を必ず呼ぶ。不要なら constructor ごと省略
super() を2回 ReferenceError: Super constructor may only be called once 1回にする
Item() と new なしで呼ぶ TypeError: Class constructor Item cannot be invoked without 'new' new Item() と書く
宣言より前で new Item() ReferenceError: Cannot access 'Item' before initialization クラス宣言を使う場所より上に移す(関数宣言と違い巻き上げで使えない)
メソッドを変数に取り出して呼ぶ const f = i.inc; f() TypeError: Cannot read properties of undefined (reading '#n') this が外れている。i.inc.bind(i) かアロー関数(下記)
getter だけのプロパティに代入 TypeError: Cannot set property stock of #<Item> which has only a getter setter を追加するか、変更用のメソッドを作る
インスタンスから static を呼ぶ i.create() TypeError: i.create is not a function Item.create() とクラスから呼ぶ
別クラスのオブジェクトの #s を読む TypeError: Cannot read private member #s from an object whose class did not declare it 先に #s in obj で確かめる

クラスの中は常に strict モードです。 そのため getter のみのプロパティへの代入は、黙って無視されずに TypeError になります。

this が外れる問題の直し方

イベントハンドラやコールバックにメソッドを渡すと、this が undefined になります。直し方は2つです。

class Counter {
  #n = 0;
  inc() { this.#n++; return this.#n; }
  incArrow = () => { this.#n++; return this.#n; };   // アロー関数のフィールド
}
const c = new Counter();
const f1 = c.inc.bind(c);   // 方法1:bind で this を固定
const f2 = c.incArrow;      // 方法2:アロー関数のフィールドは this が固定される
console.log(f1(), f2(), Object.keys(c), typeof Counter.prototype.incArrow);
// 1 2 [ 'incArrow' ] undefined
方法 長所 気をつける点
obj.method.bind(obj) prototype のメソッドを共有できる 渡すたびに書く
アロー関数のフィールド m = () => {} 取り出してもそのまま動く インスタンスごとに関数が作られる(Object.keys に出る、prototype に無い)
呼び出し側で () => obj.method() クラスを変えずに済む 呼び出し側の書き方に依存する

クラス・関数・オブジェクトリテラルの使い分け

JavaScript では、クラスを使わなくても関数とオブジェクトで同じことが書けます。 どれを選ぶかの判断表です。

状況 向いている書き方 理由
同じ形のオブジェクトをたくさん作り、状態を持たせて操作する クラス メソッドが prototype で共有され、instanceof で判定できる
状態を外から変えさせないことを保証したい クラス+# またはクロージャ どちらも実行時に隠れる
親子関係(共通処理+差分)がはっきりしている クラス+extends super で親の処理を再利用できる
入力を受けて結果を返すだけ(状態なし) 関数 テストしやすく、this を気にしなくてよい
設定値や1つしかないデータ オブジェクトリテラル { appName: '在庫管理', version: 1 } で十分
少数の状態を閉じ込めた小さな部品 関数(クロージャ) this を使わないのでコールバックに渡しやすい
JSON で受け取ったデータをそのまま渡す オブジェクトリテラル(必要なら型定義) 変換の手間が少ない
フレームワークがクラスを求める(Web Components、エラー型など) クラス class MyError extends Error のように継承が前提

関数(クロージャ)で書くと次のようになります。

const createCounter = () => {
  let n = 0;                       // 外から触れない状態
  return { inc: () => ++n };
};
const cc = createCounter();
cc.inc();
console.log(cc.inc(), Object.keys(cc));
// 2 [ 'inc' ]

迷ったら「状態を持つか」「いくつ作るか」の2点で判断します。 状態がなければ関数、状態があって複数作るならクラス、1つだけならオブジェクトリテラル、が目安です。継承は便利ですが、階層が3段以上になる場合は、部品を組み合わせる(コンポジション)書き方も検討すると変更しやすくなります。設計の考え方そのものは「オブジェクト指向とは」「デザインパターンの使いどころ」の記事で扱っています。

クラスを書くときのチェックリスト

チェック 確認ポイント
フィールドを先頭に宣言したか private は宣言が必須。public も並べておくと読みやすい
外から変えられたくない値は # か _ は読める・JSON に出る
検証が必要な代入は setter かメソッド経由か 代入させないなら getter のみ
子の constructor は super() が1行目か this より前
static private は クラス名.#x で呼んでいるか this.#x は子クラスから呼ぶとエラー
JSON に出す項目を toJSON() で決めたか private は自動では出ない
コールバックに渡すメソッドの this を固定したか bind かアロー関数のフィールド

よくある質問

Q. JavaScript に protected はありますか? A. ありません。 # の private は子クラスからも使えません。子クラスに使わせたい値は、親クラスに getter やメソッドを用意するか、TypeScript の protected を使います。

Q. #x in obj はいつ使いますか? A. 「このオブジェクトは本当にこのクラス(またはその子)が作ったものか」を例外なしで確かめたいときです。instanceof は prototype を差し替えると結果が変わりますが、#x in obj は private フィールドの有無で判定するため、より確実です。

Q. private フィールドはデバッグで見られますか? A. Node.js 22 の console.log には出ませんでした。ブラウザの開発者ツールではオブジェクトの中身として表示されることがありますが、プログラムから読めるのはクラスの中だけです。調査用に値を見たいときは getter か toJSON() を用意すると便利です。

Q. クラスのメソッドとアロー関数のフィールドはどちらがよいですか? A. 基本はメソッド(prototype で共有され、継承で super.method() も使える)です。イベントハンドラなどにそのまま渡す必要があるものだけアロー関数のフィールドにすると、両方の利点を活かせます。

まとめ

  • クラスはフィールド・constructor・メソッドの3つが基本。メソッドは prototype で共有される
  • # の private は実行時に本当に隠れる。Object.keys・JSON.stringify・子クラスからも見えない
  • _x は目印、TypeScript の private はコンパイル時だけの制限。実行時は読める
  • static はクラスに1つ。static private は this ではなくクラス名で呼ぶ
  • 子の constructor では super() を最初に。#x in obj で安全に型を確かめられる
  • 状態なしなら関数、1つだけならオブジェクトリテラル、状態があり複数作るならクラスが目安

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScriptクラス入門|private・static・継承を比較表で解説」

株式会社RJCでは、JavaScript のクラス設計を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?