JavaScript のクラス定義

公開日: : 最終更新日:2014/05/05 JavaScript ,

最近 JavaScript を使う機会が増えてきて、快適にコードが書けるように環境を少しずつ整えてきました。
その辺りはこのブログにもメモを残していて、この辺りの記事がそれになります。

で、jQuery でちょろっと表示を変えたり Ajax でサーバと通信する程度でしか JavaScript を触れてこなかったのですが、「そろそろクラス定義ぐらいちゃんと書けるようにしておかないと」と思うところがありまして。

いくつか調べて「これぐらい分かっておけばとりあえず OK なのかなぁ」って感じになってきたので、その内容をシェアしたいと思います。

icatch-class_303144538_mini

photo credit: LizMarie_AK via photopin cc

目次

1. 背景

「そろそろクラス定義ぐらいちゃんと書けるようにしておかないと」と思わせてくれたきっかけは、次の記事です。

「パターン」って言葉に惹かれてクリックしてみたんですけど、次の部分を見て「これ知っとかないとダメかも」って思いました。

モジュール・パターンは名前の通り、処理を他の処理とぶつからないように安全に切り離し、モジュールの形として提供する考え方です。

これまで Java をメインにしていたこともあって、動的言語にいまいち馴染めないところがありまして。
「この関数のパラメータは String だっけ? Integer だっけ?」みたいなところだったり、いつの間にかグローバル変数を大量に作ってたみたいな部分で不慣れを感じていたんです。
なので、こういうのを学んで少しでも改善していきたいなと思ったわけです。

2. クラス定義

※ネットを漁りまくったり JavaScript の本を読みまくったわけではないので、もっと他に良い方法があるとか、そもそも大きな間違いをしている部分があるかと思うんですが、そういうのご指摘いただけると大変ありがたいです。
JavaScript の知識増やしていきますが、何かありましたらよろしくお願いいたします。


いくつか調べたところ、上記のリンクにある「モジュールパターン」も含めて、クラス定義は3パターン程度抑えておけばいいのかなというところに落ち着いています。

2-1. パターン1: シングルトン的モジュールパターン

これは上記のリンクにあった「即時関数を使ったモジュール・パターン」ってヤツです。

「シングルトン的」と書いたのは、もう1つインスタンスを生成しようとしたらエラーになったからです。
書き方が間違っていただけなのかもしれないんですけど。。。

これをベースにちょっと検証したコートがこれです。

/**
 * モジュールパターンで定義したクラス。
 *
 * シングルトン?
 */
var ModulePattern = (function() {

    var _name; // スコープが private なプロパティ
    var _age; // スコープが private なプロパティ
    var _gender = "male"; // return で返す → スコープが public なプロパティ

    /**
     * return で返すプロパティや関数はクラスの外部からアクセス可能 → public スコープ
     */
    return {
        gender: _gender, // "gender" という名前で外部からアクセスする

        /**
         * プロパティを指定の値で初期化する。
         * 
         * @param  {string} name
         * @param  {integer} age
         */
        init: function(name, age) {
                _name = name;
                _age = age;
        },
        /**
         * プロパティを返す。
         * 
         * @return {string} プロパティの文字列結合
         */
        toString: function() {
            return _name + ":" + _age + ":" + this.gender;
        },
        /**
         * プロパティ age の setter
         * 
         * @param {integer} age 
         */
        setAge: function(age) {
                _age = age;
        },
    };

})();

参考元コードとの違いは次の通りです。

  • private なプロパティの値を外から設定できるようにした
  • public なプロパティを定義した

これのテストケースを QUnit で書きました。

module("スコープが private なプロパティの初期化");
test("疑似初期化関数を使ってプロパティの値を設定する", function() {
    // コンストラクタ関数がないので、プロパティの初期化を行う「疑似初期化関数」で値を設定する
    ModulePattern.init("hoge", 30);
    strictEqual(ModulePattern.toString(), "hoge:30:male", "init 関数で設定した値が取得できること");

});

module("プロパティのスコープ確認");
test("スコープが private なプロパティの値は直接変更できないことを確認する", function() {
    strictEqual(ModulePattern.toString(), "hoge:30:male", "以下で値を変更する前に格納されている値を確認");

    ModulePattern._age = 31; // スコープが private なプロパティの値は、クラスの外部から直接アク
セスできない
    strictEqual(ModulePattern.toString(), "hoge:30:male", "age の値は変わらないこと → 外部からアクセスできない");

    ModulePattern.setAge(31); // setter メソッドを通せば、スコープが private なプロパティであっ
ても値は変更できる
    strictEqual(ModulePattern.toString(), "hoge:31:male", "age が 31 になること");

});

test("スコープが public なプロパティの値は直接変更できることを確認する", function() {
    ModulePattern.gender = "female"; // スコープが public なプロパティの値は、クラスの外部から直接アクセスできる
    strictEqual(ModulePattern.toString(), "hoge:31:female", "male が female になること");

});
  • 擬似初期化関数を使って private スコープのプロパティに値を設定できること
  • private スコープのプロパティは、外部からアクセスできないこと
  • public スコープのプロパティは、外部からアクセスできること

以上、3点を確認しています。

このパターンのメリットは、private スコープのプロパティが使えるのでより安全なクラスが定義できるってところでしょうか。
なので、重要な責務を負わせるクラスはこのパターンを使うといいかもと思っています。

2-2. パターン2: モジュールパターン

モジュールパターンは、他にもいくつか書けるようで ↑ とは別の書き方をしたものがこれです。

/**
 * モジュールパターンで定義したクラス。
 * 
 */
var ModulePattern2 = (function() {

    var _gender = "male"; // スコープが private なプロパティ

    /**
     * コンストラクタ関数。
     * 
     * @param  {string} name 
     * @param  {integer} age 
     */
    var constructor = function(name, age) {
        this.name = name; // スコープが public なプロパティ
        this.age = age; // スコープが public なプロパティ
    };

    /**
     * prototype に関数を定義。
     *
     * ここに定義する関数はすべて public スコープになる。
     * 
     */
    constructor.prototype = {
        /**
         * プロパティを返す。
         * 
         * @return {string} プロパティの文字列結合
         */
        toString: function() {
            return this.name + ":" + this.age;
        },
        /**
         * プロパティ _gender の getter。
         * 
         * @return {string} 性別
         */
        getGender:  function() {
            return _gender;
        },
        /**
         * プロパティ _gender の setter。
         * 
         * @param {string} gender 性別
         */
        setGender: function(gender) {
            _gender = gender;
        }
    };

    return constructor;

})();

prototype が使えることがパターン1との違いです。
prototype が使えるので、インスタンスを大量に作ってもメモリにやさしい。

一見 private プロパティが使えそうに見えるのですが、private プロパティは生成したインスタンスで共有されるという罠がありました。

その辺りも検証したテストケースは次の通りです。

module("インスタンスの生成確認");
test("コンストラクタで指定した値が取得できること", function() {
    var sut = new ModulePattern2("hoge", 30);
    strictEqual(sut.toString(), "hoge:30");

});

module("プロパティのスコープ確認");
test("プロパティは public スコープなので外部から参照できることを確認する", function() {
    var sut = new ModulePattern2("hoge", 30);
    strictEqual(sut.name, "hoge", "プロパティ name を参照できること");
    strictEqual(sut.age, 30, "プロパティ age を参照できること");

});

test("プロパティは public スコープなので外部から値を変更できることを確認する", function() {
    var sut = new ModulePattern2("hoge", 30);
    sut.name = "fuga";
    sut.age = 40;
    strictEqual(sut.toString(), "fuga:40", "コンストラクタで指定した値が変更されていること");

});

module("スコープが public なプロパティの非共有");
test("スコープが public なプロパティの値がインスタンス間で共有されないことを確認する", function() {
    var hoge30 = new ModulePattern2("hoge", 30);
    strictEqual(hoge30.toString(), "hoge:30", "コンストラクタで指定した値が取得できること");

    var fuga40 = new ModulePattern2("fuga", 40);
    strictEqual(fuga40.toString(), "fuga:40", "コンストラクタで指定した値が取得できること");

    fuga40.age = 41;
    strictEqual(hoge30.toString(), "hoge:30", "fuga の age を変更しても hoge には反映されない");
});

module("スコープが private なプロパティの共有");
test("スコープが private なプロパティの値がインスタンス間で共有されることを確認する", function() {
    var hoge30 = new ModulePattern2("hoge", 30);
    strictEqual(hoge30.getGender(), "male", "gender の初期値は male");

    var fuga40 = new ModulePattern2("fuga", 40);
    strictEqual(fuga40.getGender(), "male", "gender の初期値は male");

    strictEqual(hoge30._gender, undefined, "スコープが private なので直接参照できない");
    hoge30.setGender("female"); // hoge30 の gender を female に変更する
    strictEqual(hoge30.getGender(), "female", "hoge30 の gender は female になる");
    strictEqual(fuga40.getGender(), "female", "fuga40 の gender も female になってしまう");

});
  • public スコープのプロパティは、外部からアクセスできること
  • public スコープのプロパティは、インスタンス間で共有されないこと
  • private スコープのプロパティは、外部からアクセスできないこと
  • private スコープのプロパティは、インスタンス間で共有されること

以上、4点を確認しています。

このパターンのメリットは、prototype が使えつつ、一応 private スコープのプロパティが定義できることでしょうか。
ただ、テストケースからも分かるように、private プロパティの値はインスタンス間で共有されるので、あまりメリットとは言えないかもしれません。

普段 Java をメインに使っていることもあってクラス定義が読みやすいと感じています。
今後、クラス定義のデフォルトはこのパターンにしようと思っています。

2-3. パターン3: プロトタイプを使ったコンストラクタパターン

このパターンはこれまでのボクのクラス定義のデフォルトでした。

/**
 * プロトタイプを使ったコンストラクタパターンで定義したクラス。
 * 
 * @param {string} name 
 * @param {integer} age
 */
var PrototypeWithConstructor = function(name, age) {
    this.name = name; // スコープが public なプロパティ
    this.age = age; // スコープが public なプロパティ
};
PrototypeWithConstructor.prototype = {
    /**
     * プロパティを返す。
     * 
     * @return {string} プロパティの文字列結合
     */
    toString: function() {
        return this.name + ":" + this.age;
    }
};

このパターンも prototype が使えるのでメモリにやさしいクラスになります。
private スコープのプロパティは定義できないようですが、概ねパターン2とできることは同じなのかなと思っています。

動作検証のテストケースはこんな感じです。

module("インスタンスの生成確認");
test("コンストラクタで指定した値が取得できること", function() {
    var sut = new PrototypeWithConstructor("hoge", 30);
    strictEqual(sut.toString(), "hoge:30");

});

module("プロパティのスコープ確認");
test("プロパティは public スコープなので外部から参照できることを確認する", function() {
    var sut = new PrototypeWithConstructor("hoge", 30);
    strictEqual(sut.toString(), "hoge:30", "コンストラクタで指定した値が取得できること");

    strictEqual(sut.name, "hoge", "プロパティ hoge を参照できること");
    strictEqual(sut.age, 30, "プロパティ age を参照できること");

});

test("プロパティは public スコープなので外部から値を変更できることを確認する", function() {
    var sut = new PrototypeWithConstructor("hoge", 30);
    strictEqual(sut.toString(), "hoge:30", "コンストラクタで指定した値が取得できること");

    sut.name = "fuga";
    sut.age = 40;
    strictEqual(sut.toString(), "fuga:40", "コンストラクタで指定した値が変更されていること");

});

module("プロパティの非共有");
test("プロパティの値がインスタンス間で共有されないことを確認する", function() {
    var hoge30 = new PrototypeWithConstructor("hoge", 30);
    strictEqual(hoge30.toString(), "hoge:30", "コンストラクタで指定した値が取得できること");

    var fuga40 = new PrototypeWithConstructor("fuga", 40);
    strictEqual(fuga40.toString(), "fuga:40", "コンストラクタで指定した値が取得できること");

    fuga40.age = 41;
    strictEqual(hoge30.toString(), "hoge:30", "fuga の age を変更しても hoge には反映されない");
});
  • public スコープのプロパティは、外部からアクセスできること
  • public スコープのプロパティは、インスタンス間で共有されないこと

以上、2点を確認しています。

3. まとめ

以上、JavaScript のクラス定義でした。

クラス定義の書き方1つとってもいろいろなやり方があって、この辺りの柔軟さがスッと JavaScript に馴染めない原因なんだろうなと感じています。
ずっと静的言語で育ってきた弊害なんでしょうか。

パターン privateプロパティ プロパティの非共有 prototype 備考
シングルトン的モジュールパターン o - x prototype が使えないが private なプロパティ・関数を指定できるのでガッチリしたクラスを作りたいとき。インスタンスは1つだけしか生成できないで正しい?
モジュールパターン o o o ほぼ「プロトタイプを使ったコンストラクタパターン」と同じ。クラス定義がまとまるので可読性が高い(かも)。private なプロパティは定義してもいいが値は変更しないようにする。定数扱いが無難?
プロトタイプを使ったコンストラクタパターン x o o

4. ソースコードを Github に登録しました

いつものように検証したコードは GitHub に登録してあります。

もしよければ参考にしてみてください。

5. 参考サイト

クラス定義をまとめるにあたって多くのサイトを見ましたが、その中でも特に参考にさせていただいたサイトのリンクを貼らせてもらいます。

ありがとうございました!!

5. その他の JavaScript に関する記事

JavaScript に関する記事は次の通りです。
気になる記事があったらぜひチェックしてみてください!


Googleアドセンス用(PC)

  • このエントリーをはてなブックマークに追加
  • follow us in feedly

関連記事

icatch-browser_3376956889_mini-thumbnail

GoogleMap サンプルのプロジェクトに Grunt の LiveReload を有効にする設定を追加しました

ウチのブログで公開している GoogleMaps API を使った GoogleMap のサンプルプ

記事を読む

Head in Hands

JSON をオブジェクトに変換するときに注意したいこと

おそらくですが、これから書く内容は常識なんだと思います。 が、その常識を知らなかったためにかなりの

記事を読む

icatch-template_mini

Underscore.js の template と each を使って JSON から select タグを生成する

Underscore.js を使って、都道府県名を定義してある JSON から select タグを

記事を読む

icatch-checkbox-147904_640

jQuery を使ってチェックボックスのチェックを付けたり外したりするコードスニペット

jQuery を使ってチェックボックスの ON/OFF を操作する方法ですが、以前実装したときのコー

記事を読む

icatch-drill_mini

Underscore.js の template を使ってドリルダウンを実装するスニペット

先日、Underscore.js の template と each を使って JSON から se

記事を読む

icatch-load_6161289029_mini-thumbnail

Grunt の grunt-contrib-watch を使うと CPU 使用率が上がるので spawn: false を試してみた結果

grunt-contrib-watch を使うことで、ユニットテストを実行したり、サイトをリロードし

記事を読む

icatch-googlemap_3384995399_mini-thumbnail

GoogleMap を使って住所から緯度・経度を計算する

先日も GoogleMpa のジオコーディングを使った地図を表示するメモをアップしましたが、今回も

記事を読む

icatch-painting_mini

jQuery を使ってタグに設定されているクラスをすべて取り除いた後に指定のクラスに変更する

jQuery を使ってタグに設定されているすべてのクラスを取り除いた後に指定のクラスを設定する方法で

記事を読む

icatch-where

jQuery を使ってファイルアップロードフォームのファイルが選択されているかを確認する方法

HTML に設置したファイルアップロードフォームにファイルが選択されているかを確認する方法を調べまし

記事を読む

icatch-twins_7039449789_mini

Underscore.js の template を使うときは HTML にテンプレートを書こう

Underscore.js の template メソッドを使った select タグ生成についての

記事を読む

Googleアドセンス用(PC)

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


9 × 八 =

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Googleアドセンス用(PC)

icatch-jersey_multi_pathparams
Jerseyの@PathParamはスラッシュの間に複数指定できる

http://hoge-api/user/{id}.{format}

icatch-vagrant_box_customize
VagrantのBoxファイルをカスタマイズして独自のBoxファイルを作成する

配布されている Vagrant の Box ファイルを使って検証環境を

icatch-2015-006-1
バリデーションチェックにJava8のOptionalを使ってスマートに書く(自分比)

Web アプリのバリデーションチェックにアノテーションを使うことが増え

icatch-2015-005-1
ユニットテストの偏りを防ぐ命名規則の付け方

ユニットテスト名に以下の命名規則を付けるようにして二ヶ月ぐらい経った。

icatch-2015-004-1
Vagrantで起動したCentOS上のOctopressをホストOSから確認する設定

タイトルの通りだが、Vagrant を使って起動した CentOS に

→もっと見る

PAGE TOP ↑