非オブジェクト指向で書いたコードからクラスを作ってオブジェクト指向する - Javascript

javascriptでクラスを作る方法がようやく分かった

クラスが作れるようになったから、グローバルのスコープがいろんな変数で汚れることがなくなった。プロトタイプベースで設計されているJavascriptからクラスを作る方法を簡単にまとめる*1

クラスとインスタンスの意味を分かってないと読むの辛いかも。(逆にJavaプログラマみたな人にはおすすめ)。簡単に説明するとクラスは設計図で、インスタンスは実物。ビルの設計図があって、それを元にビルを作るみたいな感じ。一つ設計図があれば、ビルをいくつも建てられるよねっていうのがオブジェクト指向のメリット。

javascriptでクラスを作る方法はいろいろあるみたいだけど、今回はprototypeを使う。この方法だと結構綺麗にかける。

ステップ

  1. 役割を整理する
  2. クラスを作る(コンストラクタを作る)
  3. 今までグローバルで定義していた関数を各クラスに所属する関数にする
  4. クラスをインスタンス化する
  5. 呼び出し方法の変更
    1. 外部からの呼び出し
    2. 内部からの呼び出し

今回使うコード

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>

<script type="text/javascript">


function init(){

	id="009";
	name="島村ジョー";

	print();
}

function print(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + id + " , Name: " + name +"<br>";
}

</script>
</head>
<body onload="init()">
    <div id = "display"></div>
</body>
</html>

今回はこれを綺麗にしていく。そんなに汚くもないっていう突っ込みはなしで。この程度のサイズだから普通に書いてもあまり問題ないけど、規模が大きくなればなるほどぐちゃぐちゃになっていく。

役割を整理する

今までぐちゃぐちゃになってたものを機能別に分けるわけだから、この作業は結構難しいかも。例えばデータ型、それを管理する部分に分けるとか。そんな風にしておおまかな機能を分ける。

今回の例だと値を初期化する部分、表示する部分がある。今回注目して欲しいのはidとnameは人を意味しているという点。だから今回は人を表すクラスを設計する。こんな風にして抽象的な物事からクラスを作っていくところがオブジェクト指向の難しいところでもある。

クラスを作る(コンストラクタを作る)

Person = function() {}

コンストラクタは、このクラスを元にしてインスタンスを作ったとき最初に実行される部分のこと。主に初期化作業を行う。こんな風にクラス名を書いて空の関数を作るだけでもいい。今回は人を表すクラスPersonを作ってみる。

Person = function(id,name) {
    this.id = id;
    this.name = name;
}

もし初期化するときに何か値を渡したいならこんな風にしてもいい。何を渡していいのかよくわからない人は初めの方法でいい。

スコープの話 パブリックとプライベート

ここで利用しているthis.idとidは別物なので注意。this.idは外部に公開できるパブリック変数になり、Personクラス内部からならthis.idという風にして参照できるし、外部からだったらperson.idという風にして参照できる。

これに対してthisのつかないidはこの関数(今回だとPersonコンストラクタ)からしか利用できない。これは頭にvarがついてる奴と同じ。

今までグローバルで定義していた関数を各クラスに所属する関数にする

例えば人の情報を表示する関数printを作る。これはPersonクラスの関数にしてしまおう。


こんな風になってる関数を…

function print(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + id + " , Name: " + name +"<br>";
}


こんな風にしてPersonに所属する関数にする

Person.prototype.print = function(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + id + " , Name: " + name +"<br>";
}

prototypeは決まりきった書き方なので覚えるしかない。ちなみにこれだけだとまだ不備があるので続きを参照。

インスタンスを作る

まだまだこれだけじゃ動かない。インスタンスを作る。上で作ったPersonクラスをインスタンス化する。

jyo = new Person("009","島村ジョー");


間違った例!!(javascriptではこんな風に変数の型を宣言してはいけない。javascriptでは型の宣言をしないのが無骨で格好いいのだ)

Person jyo = new Person("009","島村ジョー");

呼び出し元の変更

Personに含まれる値や関数を呼び出す方法。これについてはPersonクラスの内部から呼び出す方法とPersonクラスの外部から呼び出す方法が考えられる。

  • 外部からの呼び出しの変更
    • 外部からの関数呼び出し
    • 外部からの変数呼び出し
  • 内部からの呼び出しの変更
    • 内部からの関数呼び出し
    • 内部からの変数呼び出し

外部からの呼び出しの変更

外部からの呼び出しにはインスタンスを使わなきゃいけない。まずは関数の呼び出しについて取り上げる

外部からの関数の呼び出し

今まではこんな風に呼んでた

print();

だけど、printはPersonクラスのなかに隠してしまったのでこの方法では呼び出せない。


さっき作ったインスタンスjyoを使った呼び出しにする

jyo.print();

これで外部からでもこの関数を呼べる。

ID:009 , Name: 島村ジョー

こんな風に表示されるはず

外部からの変数の呼び出し

変数も呼び出せる。「スコープの話 パブリックとプライベート
」でとりあげた内容も参考に。


今まではこんな風に呼んでた

var target = name;


こんな風にしてインスタンスを使った呼び方に変えなくてはいけない

var target = jyo.name;

内部からの呼び出しの変更

次に内部からの呼び出し方法について説明する。内部っていうのはさっき定義したPersonクラスの関数内からの呼び出しのこと。

内部からの関数呼び出し

Personクラスに所属するほかの関数を呼び出してみる。今回はコンストラクタPerson()からprint()関数を呼び出してみる。

Person = function(id,name) {
    this.id = id;
    this.name = name;

    this.print();
}

こんな風にしてthisをつけて呼び出さないといけない。thisとは呼び出し元のことで、今回はPersonのインスタンスjyoがそれに当たる。要するに外部からjyo.print()を呼んでいるのと同じ。

内部からの変数呼び出し

上でとりあげたprint関数を見てみる。実はこれには不備があって、ここで指してるidとnameっていうのはPersonクラスに所属する変数ではなく、グローバルな変数になっている。

Person.prototype.print = function(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + id + " , Name: " + name +"<br>";
}


これもthisをつけて使うことで、Personに所属する変数idとnameを出力することができる

Person.prototype.print = function(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + this.id + " , Name: " + this.name +"<br>";
}

最後に

これで一通り説明は終わった。オブジェクト指向のメリットとして1つのクラスに対して複数のインスタンスを作れる点が挙げられる。下の例だともう一つ別にインスタンスを作ってる。

 jyo = new Person("009","島村ジョー");
 flan = new Person("003","フランソワーズ・アルヌール");

追記

これだけだとイベントハンドラでthisを使った場合やブロックスコープの回避に無名関数を使ってる場合不具合が生じる。
無名関数を使ったブロックスコープ化の弱点 - 犬も歩けば棒も歩く

最終的に変更したコード

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>

<script type="text/javascript">
<!--

function init(){
	jyo = new Person("009","島村ジョー");
	flan = new Person("003","フランソワーズ・アルヌール");
	
	jyo.print();
	flan.print();
}

/**
 * Personクラスの定義
 */
  
Person = function(id,name) {
    this.id = id;
    this.name = name;
}

Person.prototype.print = function(){
    //innerHTMLを使ってid="display"のタグに文字を書き込みます
    var tag = document.getElementById("display");
    tag.innerHTML= tag.innerHTML + "ID:" + this.id + " , Name: " + this.name +"<br>";
}

-->
</script>
</head>
<body onload="init()">
    <div id = "display"></div>
</body>
</html>

追記2009/4/22

ソースコードがきちんと表示されていないというご指摘をいただき,修正しました。
Kさんありがとうございます。


また,「非オブジェクト指向で設計されてるJavascript」と書いている部分があるのですが,この部分については誤りです。
正確には「プロトタイプベースのオブジェクト指向であるJavascript」です。
ちなみにJavaはクラスベースのオブジェクト指向です。

*1:当初,「非オブジェクト指向で設計されているJavaScript」と記載していましたが,この表記は誤りです.