JavaScriptでオブジェクト指向をはじめよう
JavaScriptを書くときの心得のようなものを軽くドキュメントにして欲しい、とのお達しがあったので、 勉強もかねてまとめてみることにしました。
以下を参考にして、コンストラクタパターンについて説明します。
- 作者: Addy Osmani,豊福剛,サイフォン合同会社
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/05/25
- メディア: 大型本
- この商品を含むブログ (7件) を見る
コンストラクタパターン
コンストラクタパターンでは、コンストラクタとして振る舞う関数を定義します。
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; }; });
このように、Room
のprototype
オブジェクトにメンバメソッドを定義することで、
コンストラクタ関数呼び出しの際の再定義や、メモリ上での重複を避けることが出来ます。
名前空間
このように定義したコンストラクタ関数は、定義したのとは別のスコープで利用したいかもしれません。
こういった場合、グローバルスコープに定義してしまいがちですが、何も考えずに作ったコンストラクタ関数を全部グローバルスコープに置くと、大変なことになります。
言語に関係なく言えることですが、特に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...