JavaScriptでなんちゃってstatic変数(クラス変数)を実装してみる。

todaarguments, caller, JavaScript, static変数, なんちゃって

今年の夏は暑さが身にこたえるくらいの猛暑で、半分溶けているtodaです。

さて、JavaScriptのクラスを書いていて、staitc変数(クラス変数)が必要なことがあります。
しかしながら、現在のJavaScriptにはstatic変数という概念がありません。

実は、”static”は予約語ですらありません。

ECMA-266 5th Editionのpdf(2009年12月)の”7.6.1.2 Future Reserved Words”では、JavaScriptをstatic modeとして動作させるときに”Future Reserved Words”として扱うべきトークンの一つとなっていますが、キーワードでは無いようです。

7.6.1.2 Future Reserved Words
The following words are used as keywords in proposed extensions and are therefore reserved to allow for the possibility of future adoption of those extensions.
Syntax

FutureReservedWord :: one of
       class       enum       extends       super 
       const       export     import

The following tokens are also considered to be FutureReservedWords when they occur within strict mode code (see 10.1.1). The occurrence of any of these tokens within strict mode code in any context where the occurrence of a FutureReservedWord would produce an error must also produce an equivalent error:

       implements     let         private     public     yield
       interface      package     protected   static 

実際FireFox, IE, Chromeなどで以下のようなコード記述してもエラーにはなりません。

var static = 1;

話が横道にそれましたが、JavaScriptのクラスにstatic変数もどきを実装する方法を考えました。
前述のとおり、JavaScriptにはstatic変数という概念自体が無いので、他のトリックを使う必要があります。

今回使うのは、static変数を関数オブジェクトのプロパティに保存するというトリック。JavaScriptでは関数もオブジェクトの一部で、独自プロパティを持つことができるので、これに値を保存します。

まずはソースをご覧ください。

var MyClass = new Class.create({
	initialize: function(){
		// コンストラクタ
	},

	show_static: function(){
		alert(MyClass.static());
	}
});

// static変数としてふるまう、関数オブジェクトを定義。
MyClass.static = function(v){
	if(typeof(arguments.callee.v) == 'undefined'){
		arguments.callee.v = false;		 // このfalseがデフォルト値。
	}
	if(typeof(v) != 'undefined'){
		arguments.callee.v = v;
	}

	return arguments.callee.v;
}

var o1 = new MyClass();
var o2 = new MyClass();

MyClass.static(1);		// MyClass.staticに 1をセット
o1.show_static();		// --> 1
o2.show_static();		// --> 1

MyClass.static(2);		// MyClass.staticに 2をセット
o1.show_static();		// --> 2
o2.show_static();		// --> 2

MyClass.staticは関数ですので、あくまでも()を付けて、関数として呼び出す必要があります。この関数は引数があればsetter、引数が無ければgetterとして動作します。

では、このトリックを紐解いていきましょう。

キーポイントは

MyClass.static = function(v){
	if(typeof(arguments.callee.v) == 'undefined'){
		arguments.callee.v = false;		 // このfalseがデフォルト値。
	}
	if(typeof(v) != 'undefined'){
		arguments.callee.v = v;
	}

	return arguments.callee.v;
}

で3か所でてくる、arguments.callee。
argumentsは現在コールされいてる関数にまつわる情報を集めたコンテキストであり、そのcalleeプロパティは呼び出されている関数自身を表します。クラスオブジェクトのthisに相当するものと考えると良いでしょう。

ということで、arguments.callee.vは呼び出された関数の”v”というプロパティを指すことになります。

最初のブロックである

	if(typeof(arguments.callee.v) == 'undefined'){
		arguments.callee.v = false;		 // このfalseがデフォルト値。
	}
}

は、arguments.callee.vが未定義かどうかをチェックして、未定義であればデフォルト値(false)で初期化。

次のブロックである

	if(typeof(v) != 'undefined'){
		arguments.callee.v = v;
	}

は、パラメータvが渡されているかをチェックして、渡されていればsetterとしてふるまうためarguments.callee.vを上書き。

最後にgetterとしてふるまうために、現在のarguments.callee.vを返します。

複数のstatic変数を定義したい場合に向けて、こんな関数を定義しておくと便利です。

// static変数としてふるまうための関数を生成するための関数。
var static_var = function(default_var){
	return function(v){
		if(typeof(arguments.callee.v) == 'undefined'){
			arguments.callee.v = default_var;
		}
		if(typeof(v) != 'undefined'){
			arguments.callee.v = v;
		}
		return arguments.callee.v;
	}
}

これを使うと、最初のコードは以下のように書けます。

var MyClass = new Class.create({
	initialize: function(){
		// コンストラクタ
	},

	show_static: function(){
		alert(MyClass.static());
	}
});

// static変数としてふるまう、関数オブジェクトを定義。
MyClass.static = static_var(false);		// デフォルトはfalse。