まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2010/01/30(土)JavaScriptでオブジェクト指向なメモ

適当に追記します。

雑多なおさらい

  • JavaScriptのクラスの概念はないらしい。
  • プリミティブな数字や文字列など意外は、すべてオブジェクトであり参照。
  • 参照なので代入した側でメンバ変数をいじると、代入元も書き換わる。
  • 関数を変数に代入できる。
    var f = function (x){ alert(x); }
    f("msg");    // "msg"と表示される
    

Perl5のオブジェクトに似たところがありますが、クラス名前空間(パッケージ)を持たないため、Perl5よりも簡素な実装と言えそうです。

オブジェクト(っぽいもの)の生成

Numというクラスっぽいものです。

function Num() {
	this.add = Num_add;
	this.sub = Num_sub;
	this.get = Num_get;

	// メンバ変数
	this.count = 0;
}
function Num_add(x) {
	this.count+=x;
	return this.count;
}
function Num_sub(x) {
	this.count-=x;
	return this.count;
}
function Num_get() {
	return this.count;
}

使い方。

var num = new Num();
num.add(10);
num.sub(5);
alert( num.get() );	// 5と表示される
  • newによって生成されたオブジェクトへ Tag() によってメンバ関数(メソッド代わり)を代入し、メンバ変数を初期化する。実のところ、変数に関数(の参照)が代入できるので、単なる代入でしかない。
  • 「num.add()」のようにしてメンバ関数を呼び出すと、呼び出された側はthisでオブジェクトを参照できる。
  • メンバ関数は Num() の中で宣言すると名前空間を汚さないが、見づらくなるので……。

メンバ関数をコールバックさせる 方法1

コールバックさせたいときに、どうやってオブジェクトを渡すか。

タイマーの場合。

function Class() {
	this.Run = Class_Run;
	this.DoRun = Class_DoRun;
}
function Class_Run() {
	setTimeout(this.DoRun, 1000);
}
funciton Class_DoRun() {
	alert("DoRun()");
	// 1秒後に実行されるが、this を参照できない。
}

// 実行
var obj = new Class();
obj.Run();

この例のように時間差でメンバ変数を実行させたり、onreadystatechange による非同期実行などのコールバックは this を参照できません。onreadystatechange はAjaxで盛んに使われますから、これは厄介な問題です。

これを解決するためにクロージャというものを使い、Class_Run()を次のように書き換えます。

function Class_Run() {
	var obj = this;
	var func = function () {
		obj.DoRun();
	};
	setTimeout(func, 1000);
}

変数 func に無名関数を代入し、それを呼ばせることでオブジェクト obj=this を保持させています。クロージャの概念については深入りしませんが、関数の中に変数の値ごと閉じ込めてしまうという感覚です。

  • 閉じ込める変数はvar宣言されたローカル変数でなければならない。
  • ローカル変数でない場合はグローバル変数の参照とみなされクロージャの中にそのときの値を閉じ込めることができない。
  • より正確にはvarで宣言した変数がスコープから外れるときに初めて値が閉じ込められます。

変数 obj に this を代入しているところがミソであるわけです。クロージャを使えば、引数なども閉じ込めることができます。

メンバ関数をコールバックさせる 方法2

関数を参照として呼び出すケースだけならば方法1ですべて足りるのですが、残念ながらそればかりとは限りません。

function Class() {
	this.Run = Class_Run;
	this.DoRun = Class_DoRun;
}
function Class_Run() {
	document.write('<ifreame id="frame" src="data.html" onload="DoRun()">');
}
funciton Class_DoRun() {
	var frame = document.getElementById("frame");
	// iframeでロードした data.html の中身を参照するため、
	// iframeがロードされた後でないと実行できない。
}

// 実行
var xobj = new Class();
xobj.Run();

data.html にデータが記述されており、data.html の中身をJavaScriptから参照することでうまく処理をしたいところですが、このままでは CallbackRun() からは this を参照することができません。オブジェクト指向としては致命的です。似たケースとして selectbox や checkbox の変更(onChange="")によって処理をさせたいときもあります。

やや強引な方法ではありますが、生成時にグローバルなオブジェクトの変数名を登録してしまえば解決します。

function Class(g_name) {
	this.Run = Class_Run;
	this.DoRun = Class_DoRun;

	// グローバルで参照できる変数名
	this.g_name = g_name;
}
function Class_Run() {
	document.write('<ifreame id="frame" src="data.html" onload="'+this.g_name+'.DoRun()">');
	//「<ifreame id="frame" src="data.html" onload="xobj.DoRun()">」と出力される
}
funciton Class_DoRun() {
	var frame = document.getElementById("frame");
	// 今度は xobj.DoRun() として呼ばれるため、thisを参照できる
}

// 実行
var xobj = new Class('xobj');	// xobjという変数に入ってることを知らせておく
xobj.Run();