harayu.com

Architect, Music, Movie, Web Design, Book etc…

rss-feed

JavaScript:プロトタイプベースとしてのオブジェクト指向言語

独学メモ。

「クラス」「継承」といった概念はない

Javascriptのオブジェクトは、インスタンスを生成するコンストラクタ、0個以上のプロパティ、メソッドを持つものです。で、ネイティブのJavascriptのObjectオブジェクトから機能を継承しています。

Objectオブジェクトはそれ自体はなんでもなく、新しいオブジェクトを生成するための枠組みを提供してくれる(すべての親オブジェクトである)のですが、他のオブジェクト指向言語とは違って「クラス」「継承」といった概念はなく、オブジェクトの生成はプロトタイプという概念をベースにしています。
例えば

var now = new Date();

という文があってクラスベースの言語だとnewの後にはクラスのコンストラクタが来ますが、Javascriptにはクラスはありません。じゃあコンストラクタも作れないのかというとそんなことは全然なくてただ、関数を定義するだけでほぼクラスのようなものが作れてしまうのです。

コンストラクタ ~インスタンスを初期化する関数~

クラスのようなものをここではオブジェクトの雛形と呼ぶとしてそれを作るには

function Movie() { };

と関数を定義してオブジェクト(インスタンス)を作るには

var movie = new Moive();

とすればよいだけです。しかしこれだけではプロパティやメソッドといったメンバがなく何の意味もありません。どうやって設定するのかというとthisという演算子を用いることでメンバを追加することが出来ます。

function Movie(title) {
    //thisでメンバを追加
    this.title = title;
    this.showTitle = function () {
        //thisでメンバを参照
        document.writeln(this.title);
    };
}
//newでインスタンスを作成
var movie = new Movie("不思議の国のアリス");
//メソッドを使用
movie.showTitle();

こうしたコンストラクタのことを「Movieクラス」と便宜的に呼んだりはしますが、あくまで便宜的にで。
thisは初期化されるインスタンス自身(親オブジェクト)を指すもので上の例でのtitleプロパティやshowTitleメソッドはインスタンスプロパティ、インスタンスメソッドとも言います。

しかし、上の例ではあまり好ましくない箇所があります。それは何処でしょう?

プロトタイプ ~プロパティやメソッドの雛形を作る~

上の例のようなオブジェクトの雛形を作成した場合、このコンストラクタを利用して複数のオブジェクトを作ると

var movie1 = new Movie("不思議の国のアリス");
var movie2 = new Movie("シンデレラ");
var movie3 = new Movie("ピーターパン");

movie1.showTitle();
movie2.showTitle();
movie3.showTitle();

とか仮に作った場合のようにコンストラクタを何回も呼び出してオブジェクトを作ると、関数オブジェクトがオブジェクトの数と同じだけ作られてしまうので、単にメモリの無駄遣いとなってしまいます。

他のクラスベースの言語では各オブジェクトには元になる「クラス」があり、メソッドの定義はクラスに対して行われるのでオブジェクトは複数あっても、メソッドの定義は1つ、ということになるのですが、JavascriptはObjectオブジェクトを経由して、新しいオブジェクト生成するためのコンストラクタを呼び出し、新しいオブジェクトに全てのプロパティを含めてメモリを割り当ててしまうから、といった感じでしょうか。

こういった時に役立つのがプロトタイプなのです。それは

  • オブジェクトには「prototype」というプロパティがあり、オブジェクトの雛形の情報を保存するためのオブジェクトを指し示す
  • 同じコンストラクタを持つオブジェクト間で共有される

上の例をプロトタイプを使って書き換えると

function Movie(title) {
    this.title = title;
}
Movie.prototype.showTitle = function () {
    document.writeln(this.title);
}

となり今度は同じコンストラクタからなる複数のオブジェクトを生成しても、これらは同じプロトタイプを共有するので、同じコードからなる関数オブジェクトが重複して生成される、というメモリの無駄遣いをなくすことができます。

あとは初期化演算子を使って次のように書くことも出来ます

オブジェクト名.prototype = {
    プロパティ : 値,
    プロパティ : 値,
    ・・・・
    メソッド名 : function () {
        処理
    }
};

要はprototypeの実体はコンストラクタが持つprototypeプロパティで、自由に変数、関数を追加することができるのです。

オブジェクトとクロージャ

さて、他のプログラミング言語では「スタティック変数(静的変数)」というものがあって要はそれを宣言した関数内でのみ利用でき、関数が終了してもメモリから消えない変数のことを言いますが、Javascriptではローカル変数、グローバル変数のみでスタティック変数もブロック変数もありません。

ですが、クロージャ(closure)を使えば解決できます。
じゃあクロージャとは何かというと

  1. 関数内関数であること
  2. 内側の関数の中で、外側の関数で定義した変数を使える
  3. 内側の関数が外側の関数の変数を参照している間は、外側の関数を終了しても、その変数はメモリから消えない

大雑把に言ってしまえばこんな感じです。Ajax関連で多用します。

解りにくいと思うので例を

function Counter () {
    var count = 0;
    return function () {
        count++;
        return count;
    };
}
//count変数に関数内関数の戻り値を代入
var count = Counter();
//カウンタを1増やして表示
document.writeln(ct());//1と表示
document.writeln(ct());//2と表示

で、今までの文脈でクロージャをどういう時に使うのかというと、例えばオブジェクト(コンストラクタ)の中にイベントハンドラを組み合わせる場合などです。

thisの扱い

通常コンストラクタの中のthisはそれ自身を含むインスタンスを指しますが、イベントハンドラの中ではthisは要素ノードを指します。で、このやっかいなthisをクロージャを使って解決すると。

function elmClick (elm,msg) {
    this.elm = elm;
    this.msg = msg;
    //thisをselfという変数に代入
    var self = this;
    //関数内関数なのでクロージャこの関数内ではselfを参照できる
    elm.onclick = function () {
        //this.msgだとダメなのでself.msgに
        alert(self.msg);
    };
}
var elm = document.getElementById("content");
elmClick(elm,"クリックされました");

といった形で使われたりします。

プライベートメンバ

といっても上の例で既に使用していて、var 宣言した変数はプライベートメンバです。外からアクセスできないようにし、オブジェクト内部だけで使う、要はデータを外部から隠蔽してしまうわけです。まとめると

  1. プロパティはコンストラクタの中でvar宣言する(thisじゃなしに)
  2. メソッドはコンストラクタの中で、関数として定義
  3. プライベートメンバにアクセスするメソッドはthis.メソッド名 = function(){}の形で定義

でもこれと関係なくJavaScriptで変数を宣言する時はvar をつけるのが無難でしょう。

コンストラクタの継承 apply

通常、コンストラクタの中でthisキーワードはそのオブジェクトのインスタンスを指します。なので継承元オブジェクトのコンストラクタを呼び出すと、その中でthisは継承元オブジェクトのインスタンスを指すことになってしまいます。

しかし、派生オブジェクトのコンストラクタでは、初期化するのはその派生オブジェクトのインスタンスなので、継承元オブジェクトのコンストラクタを呼び出す際にそのコンストラクタの中でも、thisが派生オブジェクトのインスタンスを指すことが必要となります。

ここで難解なapllyメソッドが登場するわけです。このapplyメソッドによって、継承元オブジェクトのコンストラクタの中でのthisが派生オブジェクトのインスタンスを指すようにしてしまうのです。

そもそもapplyメソッドとはFunctionオブジェクトのメソッドで、関数内から、他の関数を呼び出します。これによって他の関数で定義した処理を継承することができます。引数に「argument」を指定することで、自動的に元の関数が持っている引数を渡すことができます。

//継承元のオブジェクト
function Movie(title) {
    this.title = title;
}
//そのインスタンスメソッド
Movie.prototype.showTitle = function () {
    document.writeln(this.title);
}
//派生オブジェクト
function detailMovie(title,director) {
    //継承元オブジェクトのコンストラクタを呼び出す
    Movie.apply(this,[title]);
    this.director = director;
}
//prototypeプロパティの継承
detailMovie.prototype = Movie.prototype;
//複数ある場合はfor inなどを使用してもよし

と、これで継承みたいなことは一応出来たわけですが、継承元のインスタンスメソッドはtitleしか出力してくれません。なのでこのshowTitleメソッドを流用してdirectorも出力したいとすると

detailMovie.prototype.showTitle = function () {
    Movie.prototype.showTitle.apply(this,[]);
    document.writeln(this.director);
}
//今まで作ったやつを利用する
var m = new detailMovie("不思議の国のアリス","ウォルト・ディズニー");
m.showTitle();

とこんな感じでapplyの使い方の一例を示してみました。

applyとかprototypeとかはJavaScriptにおいてかなり核となる重要な項目だと思うので記事にしてみました。ふぅー。

コメントを投稿

Search

Category

Archive

Info