the glue

やってみたことで忘れそうなこと、役立ちそうなことなどをまとめています。たまに何気ない日常の話もします。

JavaScriptでオブジェクト指向をはじめよう

JavaScriptを書くときの心得のようなものを軽くドキュメントにして欲しい、とのお達しがあったので、 勉強もかねてまとめてみることにしました。

以下を参考にして、コンストラクタパターンについて説明します。

JavaScriptデザインパターン

JavaScriptデザインパターン

qiita.com

コンストラクタパターン

コンストラクタパターンでは、コンストラクタとして振る舞う関数を定義します。
JavaScriptにクラスはありませんが、このコンストラクタは実質クラス定義のようなものと考えてもらってあまり問題は無いかと思います。

たとえば、チャットルームサービスのようなものを作るとします。
サービスには複数のチャットルームがあり、それぞれのルームにユーザーが参加するものだとします。

以上を実現するために、UserコンストラクタとRoomコンストラクタを作りましょう。
UserとRoomはそれぞれ以下のようになります。

コンストラクタ関数を定義

User

  • ユーザーID・ユーザー名をもつ
var User = function (id, name) {
    this.id = id;
    this.name = name;
};

Room

  • ルームID・ルーム名をもつ
  • ユーザー管理(ユーザー参加・退室のメソッド
var Room = function (id, name) {
    // インスタンス変数
    this.id = id;
    this.name = name;

    this.users = [];

    this.join_user = function (user) {
        this.users.push(user);
    };

    this.leave_user = function (user) {
        this.users = this.users.filter(function (item, index) {
            if (item === user) return false;
            return true;
        };
    });
};

正しく動くのですが、実はこの書き方には少し問題があります。
問題のことはとりあえずあとで説明するので、このコンストラクタをどのように使うかを説明していきましょう。

コンストラクタ関数を使ってインスタンスを作成

// Roomのインスタンスを作る
var new_room = new Room(0, "hoge");
// インスタンス変数へアクセスできる
console.log(new_room.id, new_room.name); //-> 0 hoge

// Userのインスタンスを作る
var new_user1 = new User(0, "Tanaka");
var new_user2 = new User(1, "Sasaki");
// こちらもインスタンス変数にアクセスできる
console.log(new_user1.id, new_user1.name); //-> 0, Tanaka

// メンバメソッドを使う
new_room.join_user(new_user1);
new_room.join_user(new_user2);

// usersプロパティが更新されている
new_room.users.forEach(function (item. index) {
    console.log(item.name);
});
//-> Tanaka
//-> Sasaki

このように定義したコンストラクタ関数をクラスのように使うことが出来ます。
この書き方がわかりやすく、正常に動作するのですが、問題が一つあります。
メンバメソッドを、thisオブジェクトに対して定義していることです。

prototypeを使う

この問題を理解するためには、newキーワードの働きを知る必要があります。
さきほどのRoomを使って簡単に説明すると、newキーワードは以下のように働きます。

var Room = function (id, name) {

    // newをつけてコンストラクタ関数を呼び出すと
    // thisという空のオブジェクトが定義される
    // var this = {};

    this.id = id;
    this.name = name;

    this.users = [];

    this.join_user = function (user) {
        this.users.push(user);
    };

    this.leave_user = function (user) {
        this.users = this.users.filter(function (item, index) {
            if (item === user) return false;
            return true;
        };
    });

    // newをつけてコンストラクタ関数を呼び出すと
    // コンストラクタ関数は冒頭で定義されたthisをreturnする
    // return this;

};

上のように、newキーワードを使って呼び出したコンストラクタ関数では、
thisオブジェクトに対してnewするたびにメソッドが定義されます。

メンバメソッドはnewするたびに定義されるので、その分の実行時間とメモリが無駄になります。

これを解決するために、prototypeを利用します。
prototypeを利用して書き直したコンストラクタ関数がこちらです。

var Room = function (id, name) {
    // インスタンス変数
    this.id = id;
    this.name = name;

    this.users = [];
};

Room.prototype.join_user = function (user) {
    this.users.push(user);
};

Room.prototype.leave_user = function (user) {
    this.users = this.users.filter(function (item, index) {
        if (item === user) return false;
        return true;
    };
});

このように、Roomprototypeオブジェクトにメンバメソッドを定義することで、
コンストラクタ関数呼び出しの際の再定義や、メモリ上での重複を避けることが出来ます。

名前空間

このように定義したコンストラクタ関数は、定義したのとは別のスコープで利用したいかもしれません。
こういった場合、グローバルスコープに定義してしまいがちですが、何も考えずに作ったコンストラクタ関数を全部グローバルスコープに置くと、大変なことになります。
言語に関係なく言えることですが、特にJavaScriptは簡単にグローバルスコープに変数や関数などが定義できてしまうので、グローバルスコープの汚染には細心の注意を払うべきです。

こういうときには、名前空間としてグローバルに新しいオブジェクトを一つ定義し、その要素としてコンストラクタ関数を定義しましょう。
よく使われるjQuery$という名前のオブジェクトにjQueryの関数が定義されているので、$.ajax()といった具合で関数が利用できます。
さきほど定義したコンストラクタ関数を用いると、たとえば以下のような具合になります。

window.onload = function () {
    // グローバル以外のスコープ
    var MyChat = {};

    // MyChatオブジェクトにコンストラクタ関数を定義
    MyChat.Room = function (id, name) {
        // do something
    };
    MyChat.Room.prototype.join_user = function (user) {
        // do something
    };
    MyChat.Room.prototype.leave_user = function (user) {
        // do something
    });

    // MyChatオブジェクトにコンストラクタ関数を定義
    MyChat.User = function (id, name) {
        // do something
    };

    // windowオブジェクトはグローバルスコープを指します
    window.MyChat = MyChat;
};
// どこかのスコープ
var new_room = new MyChat.Room(0, "hoge");
// do something...

まとめ