JavaScriptでnew演算子をつけてもつけなくても同じようにインスタンスを作成

またJavaScriptで遊んでる。

JavaScriptにもnew演算子があった。クラスを定義してからnewでインスタンスを作成するということができるらしい。

コンストラクタになる関数を作ってからそれをnewの後に付けて呼べばいい。

function Class1(){
	this.str = "これはClass1のインスタンス";
}
Class1.prototype.toString = function(){
	return this.str;
}

var instance1 = new Class1();
alert(instance1);


コンストラクタが呼ばれたとき、thisが新しいインスタンスになっているので好きなように初期化する。


でもこれ、new書き忘れたら大変なことになりますね。Class1を普通に関数として呼んだ時のthisは何でしたっけ。ブラウザの場合はwindow?

function Class1(){
	this.str = "これはClass1のインスタンス";
}
Class1.prototype.toString = function(){
	return this.str;
}

var instance1 = new Class1();
var instance2 = Class1();
alert("instance1="+instance1+"\n"
	+"instance2="+instance2+"\n"
	+"window.str="+window.str);


これはひどい。new演算子を使う設計は危険というのも納得。


instanceof演算子を使うとthisがClass1のインスタンスかどうか調べられるので例外を投げることができる。

function Class1(){
	if(this instanceof Class1){
		this.str = "これはClass1のインスタンス";
	}else{
		throw Error("Class1 is a constructor");
	}
}
Class1.prototype.toString = function(){
	return this.str;
}

var instance1 = new Class1();
var instance2 = Class1();
alert("instance1="+instance1+"\n"
	+"instance2="+instance2+"\n"
	+"window.str="+window.str);


せっかくだから新しいインスタンスを作って返すようにする。

function Class1(){
	if(this instanceof Class1){
		this.str = "これはClass1のインスタンス";
	}else{
		return new Class1();
	}
}
Class1.prototype.toString = function(){
	return this.str;
}

var instance1 = new Class1();
var instance2 = Class1();
alert("instance1="+instance1+"\n"
	+"instance2="+instance2+"\n"
	+"window.str="+window.str);


だいぶいいですね。組み込みオブジェクトみたいにnewがあってもなくても同じ物が作れそうです。


でもまだコンストラクタの引数の数が決まってないような場合には使えない。

function Class1(){
	if(this instanceof Class1){
		this.length = arguments.length;
	}else{
		return new Class1();
	}
}
Class1.prototype.toString = function(){
	return this.length.toString();
}

var instance1 = new Class1(1, 2);
var instance2 = Class1(3, 4, 5);
alert("instance1="+instance1+"\n"
	+"instance2="+instance2);


まあ当然ですね。


一方、ECMAScript Language Specificationによれば組み込みオブジェクトのArrayでは引数の数が決まっていません。

Array ( [ item1 [ , item2 [ , … ] ] ] )
new Array ( [ item0 [ , item1 [ , … ] ] ] )

しかもnewがあってもなくても作られるオブジェクトが全く同じになるそうです。

var array1 = new Array(1, 2);
var array2 = Array(3, 4, 5);
alert("array1.length="+array1.length+"\n"
	+"array2.length="+array2.length);


自分で作ったクラスでもこうしたい。


受け取った可変個の引数を別の関数に渡すのはFunction.prototype.applyでできる。

function Class1(){
	if(this instanceof Class1){
		this.length = arguments.length;
	}else{
		var obj = new Class1();
		Class1.apply(obj, arguments);
		return obj;
	}
}
Class1.prototype.toString = function(){
	return this.length.toString();
}

var instance1 = new Class1(1, 2);
var instance2 = Class1(3, 4, 5);
alert("instance1="+instance1+"\n"
	+"instance2="+instance2);


Class1が2回呼ばれてしまうのが気に食わない。


なんかnewしつつもClass1の呼び出しはapplyでできるようなこんな感じの…

var obj = new Class1.apply(???, arguments);

無理。これだとnewされるのはClass1じゃなくてClass1.applyの方だし、この時点ではまだオブジェクトが作成されていないのでthisArg*1は書きようがない。


new演算子の中身を調べる。

var instance1 = new Class1(1, 2);

ECMAScript Language Specificationにはいろいろ書いてあるんですが、結局のところnew演算子の仕事はClass1の内部メソッド*2[[Construct]]を呼ぶことらしい。
[[Construct]]が何をするかというと…

1. 新しいオブジェクトobjを作る。
2. Class1.prototypeの値をobjの内部プロパティ*3[[Prototype]]に入れる。
3. objをthisとして与えてClass1を呼ぶ。

面倒なのを省略すればこの3つになる。
1は var obj = new Object(); でできる。3は Class1.apply(obj, arguments); でできる。2は内部プロパティに手を出さないといけないので難しそうです。FirefoxChromeでは__proto__という名前らしいんですが、言語仕様には出てきません。このままだと実装依存になってしまう。
探し回っていたら素晴らしい技が見つかりました。こういうやり方があるらしい。

var Traits = function () {};
Traits.prototype = SuperClass.prototype;
SubClass.prototype = new Traits();

何もしないコンストラクタを作ってprototypeだけ設定しておく。そのコンストラクタを使ってnewすれば余計な初期化はしないで済むというわけ。


この技を使って…

function Class1(){
	if(this instanceof Class1){
		this.length = arguments.length;
	}else{
		var Tmp = function(){}
		Tmp.prototype = Class1.prototype;
		var obj = new Tmp();
		Class1.apply(obj, arguments);
		return obj;
	}
}
Class1.prototype.toString = function(){
	return this.length.toString();
}

var instance1 = new Class1(1, 2);
var instance2 = Class1(3, 4, 5);
alert("instance1="+instance1+"\n"
	+"instance2="+instance2);


できた!


本当にClass1のインスタンスになってるのかな。

function Class1(){
	if(this instanceof Class1){
		this.str = "new演算子で作成";
		this.arg = Array.apply(this, arguments);
	}else{
		var Tmp = function(){}
		Tmp.prototype = Class1.prototype;
		var obj = new Tmp();
		Class1.apply(obj, arguments);
		obj.str = "Class1関数で作成" // 目印つける
		return obj;
	}
}

var instance1 = new Class1(1, 2);
var instance2 = Class1(3, 4, 5);
alert("instance1.str="+instance1.str+"\n"
	+"instance1.arg="+instance1.arg+"\n"
	+"Class1のインスタンス"+(instance1 instanceof Class1 ? "です" : "ではありません")+"\n"
	+"\n"
	+"instance2.str="+instance2.str+"\n"
	+"instance2.arg="+instance2.arg+"\n"
	+"Class1のインスタンス"+(instance2 instanceof Class1 ? "です" : "ではありません"));


おお、なってる。


コピペで使いまわせるようにarguments.calleeを使ったりして書く。

function Class1(){
	if(this instanceof arguments.callee){
		// ここに固有の初期化処理を書く
		this.arg = Array.apply(this, arguments);
	}else{
		var Tmp = function(){}
		Tmp.prototype = arguments.callee.prototype;
		var obj = new Tmp();
		arguments.callee.apply(obj, arguments);
		return obj;
	}
}
Class1.prototype.toString = function(){
	return this.arg.toString();
}

// 使ってみる
var instance1 = new Class1(1, 2);
var instance2 = Class1(3, 4, 5);
alert("instance1="+instance1+"\n"
	+"Class1のインスタンス"+(instance1 instanceof Class1 ? "です" : "ではありません")+"\n"
	+"\n"
	+"instance2="+instance2+"\n"
	+"Class1のインスタンス"+(instance2 instanceof Class1 ? "です" : "ではありません"));


完成。


できたけど、面倒。newを書き忘れなければいいだけだから、コンストラクタでthisをチェックして例外投げるのが最善。

*1:applyの1つ目の引数

*2:internal method

*3:internal property