JavaScriptのクラスとオブジェクト指向入門|constructorとメソッドの書き方から継承まで


はじめに

前回の記事ではGitの基本的な使い方としてバージョン管理・コミット・ブランチ・GitHubへのプッシュを解説しました。今回はJavaScriptのクラスとオブジェクト指向プログラミングの基本を解説します。「同じような処理を何度も書いている」「コードが長くなって管理しにくい」という悩みをクラスは解決してくれます。カルーセルやモーダルなど実務でよく使うUIを例に、クラスの書き方と使い方を実践的に解説します。


1. クラスとは何か

1.1 オブジェクト指向の考え方

オブジェクト指向プログラミングとは:

データ(プロパティ)と
そのデータを操作する処理(メソッド)を
ひとつの「オブジェクト」としてまとめる考え方。

クラスを使わない場合の問題点:

// カルーセルが1つなら問題ない
const track  = document.querySelector( '.carousel-track' );
let current  = 0;

function next() { ... }
function prev() { ... }

// カルーセルが2つになると...
const track1  = document.querySelector( '.carousel1-track' );
const track2  = document.querySelector( '.carousel2-track' );
let current1  = 0;
let current2  = 0;

function next1() { ... }
function next2() { ... }
// → 変数と関数が爆発的に増える
// → どれがどのカルーセルのものか把握しにくい

クラスを使うと:

const carousel1 = new Carousel( '#carousel1' );
const carousel2 = new Carousel( '#carousel2' );
// → それぞれ独立したインスタンスとして管理できる
// → コードが整理されて見通しがよくなる

1.2 クラスの基本的な構造

// クラスの定義
class クラス名 {

    // コンストラクター:インスタンス生成時に自動実行される
    constructor( 引数 ) {
        // プロパティの初期化
        this.プロパティ名 = 値;
    }

    // メソッドの定義
    メソッド名() {
        // 処理
    }
}

// インスタンスの生成
const インスタンス = new クラス名( 引数 );

// メソッドの呼び出し
インスタンス.メソッド名();

2. クラスの基本を覚える

2.1 シンプルなクラスを作る

// 人を表すクラス
class Person {

    constructor( name, age ) {
        // this = 生成されるインスタンス自身を指す
        this.name = name;
        this.age  = age;
    }

    // 自己紹介メソッド
    greet() {
        return `こんにちは!私は ${ this.name }、${ this.age }歳です。`;
    }

    // 誕生日メソッド
    birthday() {
        this.age += 1;
        console.log( `${ this.name } は ${ this.age } 歳になりました!` );
    }
}

// インスタンスを生成する
const taro  = new Person( '山田太郎', 30 );
const hanako = new Person( '鈴木花子', 25 );

// メソッドを呼び出す
console.log( taro.greet() );
// → こんにちは!私は 山田太郎、30歳です。

console.log( hanako.greet() );
// → こんにちは!私は 鈴木花子、25歳です。

taro.birthday();
// → 山田太郎 は 31 歳になりました!

// プロパティに直接アクセスする
console.log( taro.name ); // 山田太郎
console.log( taro.age );  // 31

2.2 thisの理解

/*
  this の意味はコンテキストによって変わる。
  クラスのメソッド内では
  「そのインスタンス自身」を指す。
*/

class Counter {
    constructor( initialValue = 0 ) {
        this.count = initialValue; // インスタンスごとに独立した値
    }

    increment() {
        this.count += 1;
        return this; // メソッドチェーンのためにthisを返す
    }

    decrement() {
        this.count -= 1;
        return this;
    }

    reset() {
        this.count = 0;
        return this;
    }

    getValue() {
        return this.count;
    }
}

const counter1 = new Counter();
const counter2 = new Counter( 100 );

counter1.increment().increment().increment();
console.log( counter1.getValue() ); // 3

counter2.decrement().decrement();
console.log( counter2.getValue() ); // 98

// counter1 と counter2 は独立している
// → それぞれ別のcountプロパティを持つ

3. ゲッターとセッター

class Temperature {
    constructor( celsius ) {
        this._celsius = celsius; // _ は「直接触らないで」の慣例
    }

    // ゲッター:プロパティのように値を取得する
    get celsius() {
        return this._celsius;
    }

    get fahrenheit() {
        return this._celsius * 9/5 + 32;
    }

    // セッター:プロパティのように値をセットする
    set celsius( value ) {
        if ( value < -273.15 ) {
            throw new Error( '絶対零度を下回る温度は設定できません' );
        }
        this._celsius = value;
    }
}

const temp = new Temperature( 25 );

// ゲッター(メソッドなのに()不要)
console.log( temp.celsius );    // 25
console.log( temp.fahrenheit ); // 77

// セッター
temp.celsius = 100;
console.log( temp.fahrenheit ); // 212

// バリデーション付き
try {
    temp.celsius = -300; // Error発生
} catch ( e ) {
    console.error( e.message );
    // → 絶対零度を下回る温度は設定できません
}

4. 静的メソッド(staticメソッド)

/*
  staticメソッド:
  インスタンスを生成しなくても呼び出せるメソッド。
  クラス自体に属するユーティリティ的な処理に使う。
*/

class MathUtils {

    // static をつけるとクラスメソッドになる
    static add( a, b ) {
        return a + b;
    }

    static max( ...numbers ) {
        return Math.max( ...numbers );
    }

    static clamp( value, min, max ) {
        return Math.min( Math.max( value, min ), max );
    }

    // ファクトリメソッド(インスタンスを生成して返す)
    static createRange( start, end ) {
        return Array.from(
            { length: end - start + 1 },
            ( _, i ) => start + i
        );
    }
}

// インスタンス不要でそのまま呼び出す
console.log( MathUtils.add( 3, 4 ) );              // 7
console.log( MathUtils.max( 1, 5, 3, 9, 2 ) );    // 9
console.log( MathUtils.clamp( 150, 0, 100 ) );     // 100
console.log( MathUtils.createRange( 1, 5 ) );      // [1, 2, 3, 4, 5]

5. 継承(extends)

5.1 継承の基本

/*
  継承:
  既存のクラスの機能を引き継いで
  新しいクラスを作る仕組み。

  → 共通の処理を親クラスにまとめて
    子クラスで拡張する
*/

// 親クラス(基底クラス)
class Animal {
    constructor( name, sound ) {
        this.name  = name;
        this.sound = sound;
    }

    speak() {
        return `${ this.name } は "${ this.sound }" と言いました。`;
    }

    toString() {
        return `[${ this.constructor.name }: ${ this.name }]`;
    }
}

// 子クラス(派生クラス)
class Dog extends Animal {
    constructor( name ) {
        super( name, 'ワン' ); // 親のconstructorを呼ぶ(必須)
        this.type = '犬';
    }

    // 独自メソッドを追加
    fetch() {
        return `${ this.name } がボールを取ってきました!`;
    }
}

class Cat extends Animal {
    constructor( name ) {
        super( name, 'ニャン' );
        this.type = '猫';
    }

    // 親のメソッドをオーバーライド(上書き)
    speak() {
        return `${ this.name } は気まぐれに "${ this.sound }" と言いました。`;
    }

    purr() {
        return `${ this.name } がゴロゴロ言っています。`;
    }
}

const dog = new Dog( 'ポチ' );
const cat = new Cat( 'タマ' );

console.log( dog.speak() );  // ポチ は "ワン" と言いました。
console.log( dog.fetch() );  // ポチ がボールを取ってきました!

console.log( cat.speak() );  // タマ は気まぐれに "ニャン" と言いました。
console.log( cat.purr() );   // タマ がゴロゴロ言っています。

// instanceof で型チェック
console.log( dog instanceof Dog );    // true
console.log( dog instanceof Animal ); // true
console.log( dog instanceof Cat );    // false

5.2 superの使い方

class UIComponent {
    constructor( selector ) {
        this.el = document.querySelector( selector );
        if ( !this.el ) {
            throw new Error( `${ selector } が見つかりません` );
        }
        this.isVisible = false;
    }

    show() {
        this.el.classList.add( 'is-visible' );
        this.isVisible = true;
    }

    hide() {
        this.el.classList.remove( 'is-visible' );
        this.isVisible = false;
    }

    toggle() {
        this.isVisible ? this.hide() : this.show();
    }
}

// UIComponentを継承したModalクラス
class Modal extends UIComponent {
    constructor( selector ) {
        super( selector ); // 親のconstructorを実行
        this.overlay  = this.el.querySelector( '.modal-overlay' );
        this.closeBtn = this.el.querySelector( '.modal-close' );
        this.init();
    }

    // 親のshowをオーバーライド(拡張)
    show() {
        super.show(); // 親のshow()を実行してから追加処理
        document.body.style.overflow = 'hidden';
        this.closeBtn.focus();
    }

    // 親のhideをオーバーライド(拡張)
    hide() {
        super.hide(); // 親のhide()を実行してから追加処理
        document.body.style.overflow = '';
    }

    init() {
        this.closeBtn.addEventListener( 'click', () => this.hide() );
        this.overlay.addEventListener( 'click', () => this.hide() );
        document.addEventListener( 'keydown', ( e ) => {
            if ( e.key === 'Escape' && this.isVisible ) this.hide();
        } );
    }
}

// UIComponentを継承したToastクラス
class Toast extends UIComponent {
    constructor( selector, duration = 3000 ) {
        super( selector );
        this.duration = duration;
    }

    show( message ) {
        this.el.textContent = message;
        super.show();

        // 一定時間後に自動で閉じる
        setTimeout( () => this.hide(), this.duration );
    }
}

// 使用例
const modal = new Modal( '#contact-modal' );
const toast  = new Toast( '#toast', 4000 );

document.querySelector( '.modal-open' )
    .addEventListener( 'click', () => modal.show() );

document.querySelector( '.submit-btn' )
    .addEventListener( 'click', () => {
        modal.hide();
        toast.show( '送信が完了しました!' );
    } );

6. プライベートフィールド

/*
  プライベートフィールド(#):
  クラスの外からアクセスできない
  本当の意味での非公開プロパティ。

  # をプレフィックスにつけることで
  クラスの外からは読み書きできなくなる。
*/

class BankAccount {
    // プライベートフィールドはクラスの先頭で宣言する
    #balance  = 0;
    #history  = [];
    #owner;

    constructor( owner, initialDeposit = 0 ) {
        this.#owner   = owner;
        if ( initialDeposit > 0 ) {
            this.#deposit( initialDeposit );
        }
    }

    // プライベートメソッド
    #deposit( amount ) {
        this.#balance += amount;
        this.#history.push( {
            type:   '入金',
            amount,
            balance: this.#balance,
            date:    new Date().toLocaleDateString( 'ja-JP' ),
        } );
    }

    // パブリックメソッド(外から呼び出せる)
    deposit( amount ) {
        if ( amount <= 0 ) throw new Error( '入金額は0より大きくしてください' );
        this.#deposit( amount );
        console.log( `${ amount }円を入金しました。残高: ${ this.#balance }円` );
    }

    withdraw( amount ) {
        if ( amount <= 0 ) throw new Error( '出金額は0より大きくしてください' );
        if ( amount > this.#balance ) throw new Error( '残高が不足しています' );

        this.#balance -= amount;
        this.#history.push( {
            type:   '出金',
            amount,
            balance: this.#balance,
            date:    new Date().toLocaleDateString( 'ja-JP' ),
        } );
        console.log( `${ amount }円を出金しました。残高: ${ this.#balance }円` );
    }

    // ゲッターで読み取り専用で公開
    get balance() { return this.#balance; }
    get owner()   { return this.#owner; }

    getHistory() {
        return [ ...this.#history ]; // コピーを返してオリジナルを守る
    }
}

const account = new BankAccount( '山田太郎', 10000 );

account.deposit( 5000 );   // 5000円を入金しました。残高: 15000円
account.withdraw( 3000 );  // 3000円を出金しました。残高: 12000円

console.log( account.balance ); // 12000

// プライベートフィールドには外からアクセスできない
// console.log( account.#balance ); // SyntaxError!

7. 実務でのクラス活用例

7.1 カルーセルをクラスで書く

class Carousel {
    // プライベートフィールド
    #currentIndex = 0;
    #autoPlayTimer = null;
    #isAnimating   = false;

    constructor( selector, options = {} ) {
        this.el = document.querySelector( selector );
        if ( !this.el ) return;

        // オプションのデフォルト値
        this.options = {
            autoPlay:     true,
            interval:     5000,
            loop:         true,
            ...options,   // 渡されたオプションで上書き
        };

        this.track      = this.el.querySelector( '.carousel-track' );
        this.slides     = this.el.querySelectorAll( '.carousel-slide' );
        this.btnPrev    = this.el.querySelector( '.carousel-btn-prev' );
        this.btnNext    = this.el.querySelector( '.carousel-btn-next' );
        this.indicators = this.el.querySelectorAll( '.indicator' );

        this.#init();
    }

    get currentIndex() {
        return this.#currentIndex;
    }

    get totalSlides() {
        return this.slides.length;
    }

    #init() {
        this.btnPrev?.addEventListener( 'click', () => this.prev() );
        this.btnNext?.addEventListener( 'click', () => this.next() );

        this.indicators.forEach( ( dot, i ) => {
            dot.addEventListener( 'click', () => this.goTo( i ) );
        } );

        this.el.addEventListener( 'mouseenter', () => this.pause() );
        this.el.addEventListener( 'mouseleave', () => this.play() );

        this.#initSwipe();

        if ( this.options.autoPlay ) this.play();
    }

    #initSwipe() {
        let startX = 0;

        this.el.addEventListener( 'touchstart', ( e ) => {
            startX = e.touches[0].clientX;
        }, { passive: true } );

        this.el.addEventListener( 'touchend', ( e ) => {
            const diff = startX - e.changedTouches[0].clientX;
            if ( Math.abs( diff ) > 50 ) {
                diff > 0 ? this.next() : this.prev();
            }
        }, { passive: true } );
    }

    #updateUI() {
        const offset = -this.#currentIndex * 100;
        this.track.style.transform = `translateX( ${ offset }% )`;

        this.indicators.forEach( ( dot, i ) => {
            dot.classList.toggle( 'is-active', i === this.#currentIndex );
        } );

        if ( this.btnPrev ) {
            this.btnPrev.disabled = !this.options.loop && this.#currentIndex === 0;
        }
        if ( this.btnNext ) {
            this.btnNext.disabled = !this.options.loop && this.#currentIndex === this.totalSlides - 1;
        }
    }

    goTo( index ) {
        if ( this.#isAnimating ) return;

        if ( this.options.loop ) {
            this.#currentIndex = ( index + this.totalSlides ) % this.totalSlides;
        } else {
            this.#currentIndex = Math.max( 0, Math.min( index, this.totalSlides - 1 ) );
        }

        this.#updateUI();
    }

    prev() { this.goTo( this.#currentIndex - 1 ); }
    next() { this.goTo( this.#currentIndex + 1 ); }

    play() {
        if ( this.#autoPlayTimer ) return;
        this.#autoPlayTimer = setInterval(
            () => this.next(),
            this.options.interval
        );
    }

    pause() {
        clearInterval( this.#autoPlayTimer );
        this.#autoPlayTimer = null;
    }

    // 公開API:外から現在のスライドを取得
    getCurrentSlide() {
        return this.slides[ this.#currentIndex ];
    }

    // クリーンアップ
    destroy() {
        this.pause();
        this.el.replaceWith( this.el.cloneNode( true ) );
    }
}

// 使用例
const heroCarousel = new Carousel( '#hero', {
    autoPlay: true,
    interval: 6000,
    loop:     true,
} );

const newsCarousel = new Carousel( '#news', {
    autoPlay: false,
    loop:     false,
} );

// 外から操作することもできる
document.querySelector( '.go-to-3' )
    .addEventListener( 'click', () => heroCarousel.goTo( 2 ) );

7.2 フォームバリデーターをクラスで書く

class FormValidator {
    #rules   = {};
    #errors  = {};

    constructor( formSelector ) {
        this.form = document.querySelector( formSelector );
        if ( !this.form ) return;

        this.form.addEventListener( 'submit', ( e ) => {
            e.preventDefault();
            if ( this.validate() ) {
                this.onSuccess( this.getValues() );
            }
        } );
    }

    // バリデーションルールを追加する
    addRule( fieldName, rules ) {
        this.#rules[ fieldName ] = rules;
        return this; // メソッドチェーン対応
    }

    // フィールドの値を取得する
    getValues() {
        const values = {};
        Object.keys( this.#rules ).forEach( ( name ) => {
            const field = this.form.querySelector( `[name="${ name }"]` );
            if ( field ) values[ name ] = field.value.trim();
        } );
        return values;
    }

    // バリデーションを実行する
    validate() {
        this.#errors = {};
        let isValid  = true;

        Object.entries( this.#rules ).forEach( ( [ name, rules ] ) => {
            const field = this.form.querySelector( `[name="${ name }"]` );
            if ( !field ) return;

            const value = field.value.trim();

            // 各ルールをチェック
            for ( const [ rule, param ] of Object.entries( rules ) ) {
                const error = this.#checkRule( rule, value, param );
                if ( error ) {
                    this.#errors[ name ] = error;
                    isValid = false;
                    break;
                }
            }
        } );

        this.#renderErrors();
        return isValid;
    }

    #checkRule( rule, value, param ) {
        switch ( rule ) {
            case 'required':
                return param && !value ? '必須項目です' : null;
            case 'minLength':
                return value.length < param
                    ? `${ param }文字以上で入力してください`
                    : null;
            case 'maxLength':
                return value.length > param
                    ? `${ param }文字以内で入力してください`
                    : null;
            case 'email':
                return param && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value )
                    ? '正しいメールアドレスを入力してください'
                    : null;
            case 'pattern':
                return !param.test( value ) ? '正しい形式で入力してください' : null;
            default:
                return null;
        }
    }

    #renderErrors() {
        // エラー表示をリセットする
        this.form.querySelectorAll( '.error-message' ).forEach( ( el ) => {
            el.textContent = '';
        } );
        this.form.querySelectorAll( '.is-error' ).forEach( ( el ) => {
            el.classList.remove( 'is-error' );
        } );

        // エラーを表示する
        Object.entries( this.#errors ).forEach( ( [ name, message ] ) => {
            const field    = this.form.querySelector( `[name="${ name }"]` );
            const errorEl  = this.form.querySelector( `#${ name }-error` );

            field?.classList.add( 'is-error' );
            if ( errorEl ) errorEl.textContent = message;
        } );
    }

    // サクセス時のコールバック(オーバーライド可能)
    onSuccess( values ) {
        console.log( '送信データ:', values );
    }
}

// 使用例
const validator = new FormValidator( '#contact-form' );

validator
    .addRule( 'name', {
        required:  true,
        minLength: 2,
        maxLength: 50,
    } )
    .addRule( 'email', {
        required: true,
        email:    true,
    } )
    .addRule( 'message', {
        required:  true,
        minLength: 10,
        maxLength: 1000,
    } );

// onSuccess をオーバーライドして送信処理を追加
validator.onSuccess = async ( values ) => {
    const response = await fetch( '/api/contact', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify( values ),
    } );
    if ( response.ok ) alert( '送信が完了しました!' );
};

8. 実践チェックリスト

✓ class キーワードでクラスを定義して new でインスタンスを生成できるか

✓ constructor でプロパティを初期化して this で参照できるか

✓ メソッドの中で this が何を指しているか理解しているか

✓ static メソッドとインスタンスメソッドの違いを説明できるか

✓ extends と super を使って親クラスを継承できるか

✓ # プレフィックスでプライベートフィールドを定義できるか

✓ get / set でゲッター・セッターを定義できるか

✓ カルーセルやモーダルなどのUIコンポーネントをクラスとして実装できるか

✓ オプション引数にデフォルト値を設定してカスタマイズ可能な設計にできるか


まとめ

今回はJavaScriptのクラスとオブジェクト指向プログラミングの基本を解説しました。ポイントをまとめると:

  • クラスはデータ(プロパティ)と処理(メソッド)をひとまとめにする仕組みで new でインスタンスを生成する
  • constructor はインスタンス生成時に自動実行されるメソッドで this で自分自身のプロパティにアクセスする
  • get / set でゲッター・セッターを定義するとプロパティのように値の取得・設定ができる
  • static をつけるとインスタンス不要で呼び出せるクラスメソッドになる・ユーティリティ関数のまとめに便利
  • extends で親クラスを継承して機能を拡張できる・super で親のコンストラクターやメソッドを呼び出す
  • # プレフィックスでプライベートフィールドを定義するとクラスの外からアクセスできなくなる
  • カルーセル・モーダル・フォームバリデーターなどのUIコンポーネントをクラスで実装すると複数設置や設定の変更が楽になる
  • オプション引数にデフォルト値を設定してスプレッド演算子でマージするパターンが実務でよく使われる

次の記事では、JavaScriptのDOM操作を徹底解説します。要素の取得・作成・追加・削除から、テンプレートリテラルを使った動的なHTML生成まで、実践的なガイドをお届けします。お楽しみに!

コメント

タイトルとURLをコピーしました