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-checklist_4439276478_mini-thumbnail

BootstrapValidator の基本的なバリデーションを試すサンプルを作成しました

TwitterBootstrap3 用のバリデーションチェックプラグイン BootstrapVali

記事を読む

icatch-plug_8436280178_mini-thumbnail

TwitterBootstrap3 用のバリデーションチェックプラグイン BootstrapValidator の導入から Live チェックまでを試してみた

BootstrapValidator は、TwitterBootstrap3 で構築したサイト用の

記事を読む

icatch-googlemap_3384995399_mini-thumbnail

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

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

記事を読む

icatch-template_mini

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

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

記事を読む

icatch-automatic_2714993937_mini-thumbnail

Grunt + QUnit + PhantomJS でテストを自動実行してくれる環境を構築する

先日投稿した GoogleMap サンプルのプロジェクトに Grunt の LiveReload を

記事を読む

icatch-format_mini-thumbnail

JavaScriptなどをフォーマットするGruntプラグイン grunt-jsbeautifier を使ってみる

Java を使ってプログラムを書いているときはソースコードをフォーマットするのに、JavaScrip

記事を読む

icatch-bootstrap3

TwitterBootstrap3 をベースにした画面に GoogleMap を埋め込む

このところ立て続けに GoogleMap を JavaScript から扱う記事を投稿してます。 意

記事を読む

icatch-simcity_536954375_mini-thumbnail

GoogleMapsAPIを使ってMarkerを削除する方法

GoogleMaps API(v3) を使って地図上に表示されている Marker を削除する方法に

記事を読む

icatch-darts_1543970905_mini-thumbnail

GoogleMapsAPIを使って緯度・経度からMarkerを作成する方法

GoogleMaps API(v3) を使って、指定した緯度・経度の地点に Marker を表示する

記事を読む

icatch-flag_1395019914_mini-thumbnail

GoogleMaps の JavaScript API(v3) を使って、常に1つの InfoWindow だけ表示されるように制御する

前回アップした GoogleMaps の JavaScript API (v3) を使って地図上に複

記事を読む

Googleアドセンス用(PC)

Message

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


+ 一 = 3

次の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 ↑