ECMAScript - on Surface of the Depth -

この言語は、HTML内に埋め込んで動きのあるページを記述するための 「JavaScript」 として知られることが多いでしょう。しかし有名すぎるためか、 意外と面白いプログラミング言語であることはあまり気付かれていないように思います。 そこでここでは、HTMLをどうこうという話は置いておいて、 言語自体についてちょっとだけ深く見ていきましょう。

  1. 処理系選び
  2. 概観
  3. * 関数リテラル
  4. 関数オブジェクト
  5. arguments
  6. オブジェクト isa 連想配列
  7. コンストラクタ
  8. prototype
  9. * No prototype!
  10. スコープチェイン
  11. その他色々
  12. * いくつかのさんぷる

処理系選び (2002/12/6)

対象とする言語仕様は
 「ECMA-262 ECMAScript Language Specification 3rd Edition」
とします。 TAKI氏による 邦訳 を参考にすると読みやすいです。

4th edition も提案されていますが、いわゆる「ECMAScriptでオブジェクト指向」 の典型パターンをclassとして文法面から記述できるようにしたのと、 静的な型指定が入ったくらいで本質的にそんなに変化はないので…。 むしろ余計な構文が導入されたせいでわかりにくくなっているため、 今のところ、個人的には好きになれなくて。(^^;

この仕様にほぼ対応している処理系としては、

などがあるようです。Mozilla6、Opera6 以降の内蔵エンジンも、だいたいOK。 IE6 で単に type="text/javascript" とした時のエンジンや、 NJS は多少怪しげ。 他に DMDScriptDMonkey は文法面ではECMAScriptのサブセット+α、という形になっているようで、 以下に書く重箱の隅のような話は通らないことも多いです。 とりあえず以下では、名前が気に入った Rhino を使って色々実験していきます。が、一応規格外のことは、出力関数 (print) を除いて、避けています。

Rhinoは、ダウンロードして解凍して java -jar js.jar すれば動きます。簡単なので、皆さんも是非ダウンロードしておためしあれ。

基本的な雰囲気概観1 (2002/12/7)

ECMAScript系の言語を知らない方のために、まずは概観を。

…といっても、一から全部入門を書くのは大変なので、C++/Java/C# のどれかの経験者であることを前提に説明させていただきます。 該当されない方、申し訳ありません。m(_ _)m

さて、概観といっても、次のサンプルを見れば一発で把握できると思われます。


function repeat_print( n, str )
{
	for( i=0; i<n; ++i )
		print( str );
}

repeat_print( 10, "hello!" );

要は、

以上です。なお、このコードは次のようにも書けます。


function repeat_print( n, str )
{
	for( var i=0; i<n; ++i )
		print( str )
}

repeat_print( 10, "hello!" )

後者には要注意。一文を下手に複数行に分けると、勝手に分割されちゃいます。 これはやめて欲しいなぁ、と思うのですがとりあえず今はあきらめるしか。 でも、せっかくあるのだからとつい使ってしまうのも人情です。--;

(※ Automatic Semicolon Insertion (ECMA262 7.9))

基本的な雰囲気概観2 (2002/12/7)

あとは、オブジェクト指向的な文法だけ軽く見ておきます。


var s = new String( "Hello" );
print( s.toUpperCase() );

まぁ、ご覧の通りです。


var s = new String( 'Hello' );
var t;
with( s )
{
	t = toUpperCase();
}
print( t );

with構文があります。意味は見たまんま。toUpperCase という名前の検索対象が、 Global関数だけでなく、s のメソッドにも広がるようになります。 あとそういえば、文字列リテラルはダブルでもシングルクォートでも同じ意味です。

上の例ではString()でオブジェクトを作りましたが、 文字列リテラルや数値リテラルは元々全てオブジェクトなので、 上の例は var s = 'Hello'; でも同じことです。

さて、準備は整ったので、いよいよ次回から Surface of the Depth へ…。

名無し~3.関数 (2002/12/8)

さきほど挙げた関数定義の例(再掲)


function repeat_print( n, str )
{
	for( var i=0; i<n; ++i )
		print( str );
}

ですが、これは次とほぼ等価です。


var repeat_print = function( n, str )
{
	for( var i=0; i<n; ++i )
		print( str );
}

function(...){ ... } で、 名前無しの関数オブジェクトを生成することができて、 それを変数に代入しているわけです。これは何を意味するかというと、 当然次のようなことができます。


function repeat_call( n, f )
{
	for( var i=0; i<n; ++i )
		f(i);
}

repeat_call( 10, function(x){print('hello'+x)} );

イベントハンドラやら作用関数やらをその場で作ってしまうのに便利なので、 今時の言語には必須の機能ですが、このあたりはきっちり押さえてあります。 余談ですが、名前付きにもできます。


function repeat_call( n, f )
{
	for( var i=0; i<n; ++i )
		f(i);
}

repeat_call( 10, function print_hello(x){print('hello'+x)} );

この場合は名前をつけたからなんだ、という気がかなりしますが、 再帰呼び出しを行う関数をその場で作りたい時には威力を発揮します。

(※ Function Definition (ECMA262 13))

関数オブジェ (2002/12/9)

関数もオブジェクトの一つです。このページではまだ ECMAScript のオブジェクトモデルについて解説していないのでやや意味不明ですが、 一応、関数定義式で作ったときの constructor は Function になります。 定義された時点から備わっているプロパティは、


function f( x ) { return x*x; }
print( 'f is ' + f.length + '-ary function' );

length。引数の個数を返します。この場合なら 1 ですね。arity というもっと良い名前のプロパティを用意している処理系もありますが、 これは標準ではありません。

(※ Properies of Function Instances - length (ECMA262 15.3.5.1))

このようなプロパティはありますが、どう定義された関数であっても、 呼び出し側では任意個数の引数を渡すことができる、ことになっています。


function f( x ) { return x*x; }
var n = f(1, 2) + f(); // 1 + NaN
print( n );

多い分は無視されて、足りない分は undefined 値になります。 ここら辺型好きーな人には我慢ならないところかと思います。(^^;

(※ Function Calls (ECMA262 11.2.3))

あと、メソッドとして call と apply があります。


function add(x, y) { return x+y; }
print( add.call(null, 10, 20) );

f.call( obj, arg1, arg2, ... )obj.f( arg1, arg2, ... ) と同値です。apply はこれの引数を配列で渡す版です。


function add(x, y) { return x+y; }
print( add.apply(null, [10, 20]) );

第一引数は、関数内で this として指されるオブジェクトの指定です。 nullかundefinedならグローバルオブジェクトが使われます。 グローバル関数はグローバルオブジェクトのメソッドみたいなもんってことですな。

引数っていんすうと読みたくなる (2002/12/9)

関数が呼び出されると、仮引数が束縛されるのとだいたい同じタイミングで、 arguments という名前のオブジェクトが生成されます。呼び出された関数の中で、 その実行時の情報を色々と得ることができます。


function accumulate()
{
	var sum = 0;
	for( var i=0; i<arguments.length; ++i )
		sum += arguments[i];
	return sum;
}
print( accumulate(1,2,3,4,5,6,7,8,9,10) );

配列のように length と [] を用いてアクセスして、 可変個引数の関数を作ることができます。


var x = ( function(a, b) {
		return (a==0 ? b : arguments.callee(b%a, a));
	} )( 60, 84 );
print( x );

calleeプロパティは呼ばれている関数自身になります。 これを使うと、上のユークリッドの互除法のように、 無名関数の再帰呼び出しが可能になります。もっとも、 前に書いたように関数リテラルにも名前がつけられるので、 必須の機能というわけでもありませんが。

呼び出し元関数を表すcallerプロパティも昔はあったそうですが、 現在は規格から外されています。

(※ Arguments Object (ECMA262 10.1.8))

関数の話のまとめ (2002/12/9)

次からは別の方向へ話を進めますので、ここらで簡単なサンプルでも。 まず、配列arrの各要素に関数fを適用して、その結果を返す関数です。普通です。


function map( arr, f )
{
	var newArr = new Array(arr.length);
	for( var i=0; i<arr.length; ++i )
		newArr[i] = f( arr[i] );
	return newArr;
}

次の例、C++erな人には任意個の引数に対応した mem_fun_ref と言えばおわかりかと。 obj.mf( a1, a2, ... ) の形で使う関数mfを f( obj, a1, a2, ... ) の形へ変形するアダプタです。

最初の3行で丸ごとコピーしてるのが非常に格好悪い無名関数が、 新しい関数オブジェクトになっています。mf.apply の mf は外側の環境で引数として与えられている点に着目。 いわゆるClosureです。たぶん後ほどもっと詳しく紹介します。


function mem_fn( mf )
{
	return function(obj) {
		var args = new Array(arguments.length-1);
		for( var i=1; i<arguments.length; ++i )
			args[i-1] = arguments[i];
		return mf.apply( obj, args );
	};
}

まぁそういうものです。


x = map(
	['hoge', 'fuga', 'hagyo'],
	mem_fn( String.prototype.toUpperCase )
);
map( x, print );

オブジェクト isa 連想配列 (2002/12/14)

まずECMAScriptにおける配列の話。


var x = new Object();
x['hello'] = 'world';
x['hoge'] = 'fuga';
print( x['hello'] );

任意のオブジェクトについて、[]に文字列を入れればそれだけで、 文字列をキーとする連想配列として使えます。というか、 文字列でなくて数字でもオブジェクトでも何でもキーとして使えます。

特に普通の0から始まる数字の列をキーとした配列を作りたいときは、 こんな風になります。


var arr = [1,2,4,8,16];
print( arr.length );
print( arr[0] + arr[3] );

それぞれ 5 と 9 と表示されると思います。[x,y,z,...] が配列リテラルになります。配列リテラルは constructor を Array() とするオブジェクトで、length プロパティがあって、あと [0] とか [1] とかで各要素にアクセスできます。

さて、続けて


var arr = [1,2,4,8,16];
print( arr['length'] );

length というプロパティには、arr['length'] としてアクセスできます。要するに、ECMAScriptにおける 'プロパティ' や 'メソッド' は、全てその名前をキーとして、 オブジェクトを連想配列と見なしてアクセスすることができます。 bbbが識別子として有効な名前なら、aaa.bbbaaa['bbb'] と等価になります。ドット記法は、 単なるシンタックスシュガー。


var str = 'aiueo';
print( str['toUpperCase']() );

(※ Property Accessors (ECMA262 11.2.1))

コンスとラクタ (2002/12/17)

先ほどまではStringにObjectと、既に定義されている方法でのみ、 オブジェクトを作成してきました。ここらで自分で「二次元の点を表すオブジェクト」 の作り方を見てみます。次の通り。


function Point()
{
	this.x = 0;
	this.y = 0;
	this.distanceFromOrigin = function()
	{
		return Math.sqrt( this.x*this.x + this.y*this.y );
	}
}

var pt = new Point();
pt.x = 5;
pt.y = -12;

print( pt.distanceFromOrigin() );

たぶん13と表示されます。

Point() はご覧の通り普通の関数です、が、中で this という特殊な変数を扱っている点と、呼び出し時に new が頭についている点が異なります。


print( pt.constructor.name );

変数 pt の指すオブジェクトのコンストラクタは Point である、と。

以上のやり方がECMAScriptにおけるPointクラスの定義方法である、 という言い方をたまに耳にしますが、それは違うのではないかなぁ、 と個人的には思います。あくまでオブジェクトを[作る]関数であって、 オブジェクトのプロパティやメソッドの定義を行っているわけではないので。 コンストラクタを通ったあとでも、 プロパティはユーザー側で勝手に付けたり消したりできちゃいますし。要するに constructorはArray()なのにあたかもDate()から作られたかのような属性を持った object、も作れるので、コンストラクタは生成されたinstanceのclass(分類) の役は果たしていないのです。つまりこやつはクラスではない

(※ Objects (ECMA262 4.2.1))

プロとタイプ (2002/12/18)

上で上げたPoint()では、ご覧の通り、呼び出されるたびに function(){ return Math.sqrt( ... ); } という関数リテラルを評価して、その結果の関数オブジェクトを this.distanceFromOrigin に入れています。つまり、 new Point() でオブジェクトを作るたびに、違う関数が作られて、 それぞれのオブジェクトのメンバ関数となる、わけです。

ちょっと考えると、これには無駄があることがわかります。 どの distanceFromOrigin も全く同じ関数なわけですから、 オブジェクト毎に別々に持っている必要はありません。 今の場合は関数一つずつなのでまだマシですが、 メソッドの10個や20個あるオブジェクトを作りたくなったら、 これはメモリを無駄に食い過ぎてしまいます。

解決策は、次の通り。


function Point()
{
	this.x = 0;
	this.y = 0;
}

Point.prototype.distanceFromOrigin = function()
{
	return Math.sqrt( this.x*this.x + this.y*this.y );
}

var pt = new Point();
pt.x = 5;
pt.y = -12;

print( pt.distanceFromOrigin() );

コンストラクタ関数の、prototypeというプロパティに関数を入れておきます。 このPoint.prototypeは1つしかないオブジェクトで、Point から作られた全てのオブジェクトに参照されますので、関数の実体は1個だけで、 複数のオブジェクトをうまくさばけるようになります。

(※ Objects (ECMA262 4.2.1))
(※ Prototype (ECMA262 4.3.5))

以下細かい話

別の言い方をすると、obj.abc という式の値としては、 objにプロパティabcが存在するかどうかまず調べ、見つからなければ次に 「objのプロトタイプ」にプロパティabcが存在するかどうか調べ、無ければ次に 「objのプロトタイプのプロトタイプ」にプロパティabcが存在するかどうか調べ… となっています。 [Prototype-Based Inheritance] というページの図がわかりやすいです。

「objのプロトタイプ」は必ずしも obj.constructor.prototype と同値ではありません。(変数objを介しての ECMAScript 側からの明示的なアクセスは不可能で、一般には暗黙のうちにリンクされます。) が、まぁ普通に自分で作ったオブジェクトなら基本的には大丈夫です。

private ~ 或いはしかしprototypeを使わない (2002/12/22)

上の、「コンストラクタの外でprototypeにメンバ関数を追加する」 というのが「ECMAScriptでオブジェクト指向」 の一番スタンダードな方法だと思われます。しかし、 効率とかは全く気にしないひねくれ者の私としては、 ここで異論を唱えてみたい。

private

上で紹介した方法では、private変数/メソッド を作成することができません。 this.xxx = ... としたのでは、必ず外からも obj.xxx と、 アクセスできてしまいます。privateにするには、

とやってアクセスされにくくする、のが手です。が、 次の素直な手段はどうでしょう?


function Point()
{
	var x = 0;
	var y = 0;

	this.getX = function() { return x; }
	this.getY = function() { return y; }
	this.set  = function(i_x, i_y) { x=i_x; y=i_y; }
}

var pt = new Point();
pt.set(100, 200);
print( pt.getX() + ' ' + pt.getY() );

x と y は、Pointの内部からのみ使えます。 (pt.x などと書くとundefinedになることを確認できます。)

要はxとyはコンストラクタ関数内のローカル変数なので、 コンストラクタのスコープ内でしか利用できない、ということですね。 C/C++などは、ローカル変数は実行がスコープから外れると破棄されてしまいますが、 ECMAScriptではその変数に対する参照が残っている間は実体が保持されますので、 getX() などからは何度でもx,yにアクセスできます。(詳しくは次回)

しかしこれは、こうは書けません。


function Point()
{
	var x = 0;
	var y = 0;

}
Point.prototype.getX = function() { return x; }
Point.prototype.getY = function() { return y; }
Point.prototype.set = function(i_x, i_y) { x=i_x; y=i_y; }

getXやsetに入る関数がPoint()の外にあるため、 Pointの中のxやyは見えなくなってしまいます。


function Point()
{
	var x = 0;
	var y = 0;

	Point.prototype.getX = function() { return x; }
	Point.prototype.getY = function() { return y; }
	Point.prototype.set = function(i_x, i_y) { x=i_x; y=i_y; }
}

こんなことをやってみても、やっぱり駄目です。Pointの中の x, y は無事見えるようになりますが、そもそも「毎回関数リテラルを作らなくていい」 というprototypeの利点が無くなってしまいますし、


var pt1 = new Point();
var pt2 = new Point();
pt1.set( 100, 200 );
pt2.set( 300, 400 );
print( pt1.getX() + ' ' + pt1.getY() );

とやると、300, 400 が出力されてしまったりして困ります。 (二回目のnew Point()を実行した時点で、 Point.prototype.getX は二回目のvar x…つまりpt2のx… を読みに行く関数になってしまっています。ので、pt1.getX() でも pt2のxの値が表示されてしまうというわけ。)

というわけで

メソッドを prototype に登録するのをやめれば、何も苦労せずに private変数/関数が作れることがわかりました。ついでに、 メソッドの中でメンバ変数にアクセスするときに、this が要らなくなるのもそれなりに綺麗かなぁ、と。

なんでこうなったかと言うと、xというプライベート変数はオブジェクト毎に [違う] ものなわけで、それならばxを[知っている]関数どうしも、 オブジェクト毎に違うものである方が自然だから、ではないかと思います。 経験上、自然な書き方をした方が往々にしてコードは綺麗になります。

欠点

関数のマトメのところで書いた、mem_fn のような関数が使えなくなります。 それぞれのオブジェクトが別々のメンバ関数を持っている、 という厄介な状況を自分で生みだしてしまったのだから、自業自得ですが。^^; これを解決するには、「オブジェクト isa 連想配列」であることを思い出して、 メンバ関数自体の代わりに、メンバ関数の名前を渡すようにすればオッケーです。


...

function mem_fn( mf )
{
	return function(obj) {
		...
		return obj[mf].apply( obj, args );
	};
}

x = map(
	['hoge', 'fuga', 'hagyo'],
	mem_fn( 'toUpperCase' )
);
map( x, print );

結論

まぁ、趣味の問題なので割とどっちでもいいかなぁという気はします。

追記

(Creating Function Objects (ECMA262 13.2))によれば、 上記の var による方法は処理系によっては成立しません。 If there already exists an object E that was created by an earlier call to this section's algorithm, and if that call to this section's algorithm was given a FunctionBody that is equated to the FunctionBody given now, then... ソースコードの全く同じ部分にある(equatedな) 関数リテラルは、2回目以降で1回目と同じオブジェクトを返すことが、 実装には許されています。つまりprototypeの時と同じで、 上手くいかない可能性があります。

が、続けてこう書かれています。 「Step 1 allows an implementation to optimise the common case of a function A that has a nested function B where B is not dependent on A.」 Aの内部関数BがAに依存しないとき、の最適化を許しています、と。 が、他ではそれ以上の最適化も許しているように読めるのでここは飛ばすとして、^^;

In practice it's likely to be productive to join two Function objects only in the cases where an implementation can prove that the differences between their [[Scope]] properties are not observable, so one object can be reused. 」現実問題としては、二つの関数オブジェクトの [[Scope]] に違いが見られないことが証明できるときのみ、2つを結合するのが生産的だろう、 となっています。これに従えば上の例では2つは違うScopeを持ちますから、 同一化されることはありません。

というわけで、すぐ上に書いた方針を採る処理系でなら問題はありません、 (実際今のところ、それ以上のことをやる処理系は存在していないようです。) が、一応注意が必要です。 In practice とか言ってないでそういう仕様にして欲しかったところですが…。

スコープチェイン (2002/12/25)

なんだか格好良さげな名前はついてますが、 実体は実に当たり前なのが、「Scope Chain」


function f1()
{
	var x = 1;
	function f2()
	{
		var y = 2;
		function f3(z)
		{
			return x + y * z;
		}
		return f3;
	}
	return f2();
}

var f = f1();
print( f(10) );

関数 f3 が実行されるときには、x という変数が必要になったら、 まず f3 で変数xが定義されているかどうか調べ、 見つからなかったら次に、f3 が定義されている関数 f2、の中で x が定義されているかどうか調べて、それでも見つからなかったら、 f2 が定義されてる関数 f1、の中で x を探し…と、 グローバルに到達するまでひたすら上にたどっていく動作のことを言います。

関数の呼び出し関係を上にたどる…のではないことに注意してください。 例えば上の例では、関数 f3 は結局 f として、グローバルから直接呼んでいますが、 f3→f2→f1→Global と、あくまで、ソース上にかかれた定義位置の関係に従って、 探索が進みます。

(※ Scope Chain and Identifier Resolution (ECMA262 10.1.4))

eval の話

それだけでは普通すぎて面白くないので、一点だけ細かいところを。

スコープチェイン(変数名→値への割り当て)が変化するのは、 「通常関数呼び出し時」「with文に入るとき」「catch節(後述)に入るとき」 の3種類のどれかのみとなっています。他の理由で変化することはありません。

といっても、そもそもその3つくらいしか変化が起きそうなものがない… と思われるかもしれません。が、さにあらず。スクリプト言語ですから、 eval という例外的な関数があります。


eval( 'print(12345)' );

print(12345) をその場で解釈して実行するプログラム。 こいつの場合も、スコープチェインの変化は起きないそうです。 どういうことかというと、


function X_Str_Obj()
{
	this.x = 'hyaku';
	this.ev = function(s) { eval(s) ; }
}

function X_Int_Obj()
{
	this.x = 100;
	this.ev = function(s) { eval(s); }
}

function Test()
{
	this.x = null;
	this.f = function()
	{
		var xs = new X_Str_Obj();
		var xi = new X_Int_Obj();
		xs.ev('print(typeof(this.x))');
		xi.ev('print(typeof(this.x))');
	}
}

(new Test()).f();

evalに文字列として渡された中の変数名は、 evalが定義された関数の中の変数名と対応する…のでも、 evalに渡す文字列が作られた場所の変数名と対応する…のでもなく、 evalが呼び出されたその時点のスコープチェインに基づいて探索されます。 上の例ならば、string, number と表示されるはず。


var x = 1;
var s = 'x';
function f(){
	var x = 0;
	print(eval(s));
}
f();
print(eval(s));

この例ならば、0, 1 の順に表示されます。

(※ Eval Code (ECMA262 10.2.2))

Function の話

また、evalに似た機能として、Function() による、 文字列から関数を作成する機能もあります。この Function() で作られた関数は、 グローバルで定義されたのと同じスコープチェインを持ちます。


var x = 1;
function test()
{
	var x = 2;
	var f = Function('return x');
	print( f() ); // 2じゃなくて1が出る
}
test();

(※ Function Constructor (ECMA262 15.3.2))

落ち穂拾い (2002/12/27)

オブジェクト初期化子。{プロパティ名:値, ...} でオブジェクトを作れます。 あと、for ~ in 文。要はプロパティの列挙です。この方向には for-in で列挙される/されないなどの「プロパティの属性」という細かい話があったりします。 が、面白くないしあんまり好きな仕様でもないので略。^^;


var obj = {x:100, y:10, name:'taro'};
for( attr in obj )
	print( attr + ' = ' + obj[attr] )

例外処理。変数に型がないので catch 節を複数書けなくて色々と面倒とか。


for(var i=0; i<2; ++i)
{
	try
	{
		if( i==0 ) throw 12345;
	}
	catch( e )
	{
		print( 'catched ' + e );
	}
	finally
	{
		print( 'finally...' );
	}
}

おもしろい処理系拡張 (2003/01/04)

処理系依存ですが、わりと面白い機能を幾つか紹介します。

function *.prototype.*(){}

Microsoftの処理系 (JScript, JScript.NET) など。prototype のプロパティには、下のような関数定義文で直接関数をセットできます。 何故かprototypeに対して以外は使えません。便利ですが謎です。

// written for JScript.NET
function Point()
{
	this.x = 0.0;
	this.y = 0.0;
}

function Point.prototype.distanceFromOrigin()
{
	return this.x*this.x + this.y*this.y;
}

WScript.Echo( (new Point()).distanceFromOrigin() );

Function.arity

Netscape系の処理系(JavaScript, rhino, SpiderMonkey)など。 前の方で紹介しましたが、関数オブジェクトに、length の別名として arity という定義時の引数の個数を与えるプロパティがあります。

// written for Rhino
function add(x, y)
{
	return x + y;
}
print( add.arity );

Function.prototype['arity'] = function(){ return this.length } 等とプログラムの頭に書いておけば、他の処理系でも add.arity() の形なら使えるようになりますが。

LiveConnect

IE, Netscapeなどのブラウザ、rhino, spidermonkeyなど。 独自拡張と言うよりは、むしろECMAScriptとは別の技術、 と言ってもよいような気がしますが、ScriptからJavaを操作することができます。

// written for Rhino
importPackage(java.awt);

var frm = new Frame('Hello');
var btn = new Button('World');

frm.setSize(200,200);
frm.add( btn );

var act = { actionPerformed: function(){quit()} };
btn.addActionListener( java.awt.event.ActionListener(act) );

frm.show();

rhinoからjava.awtを使う例。ブラウザからなら、アプレットの操作に使えます。

継続 (2004/02/06追記)

ApacheのCocoonプロジェクトの一貫として、Rhinoに拡張を加えた RhinoWithContinuations が開発されています。これはRhinoのJavaScriptに 「継続」という言語機能を付け加えて強化したもの。

// written for RhinoWithContinuations
function capture_cc()
{
    print( "Current continuation captured" );
    return new Continuation();
}

var k = capture_cc();
if( k instanceof Continuation )
{
    print("1st");
    k(10); // k = 10 にして、capture_ccでcaptureした場所から実行を続ける
}
else
{
    print("2nd")
}

__proto__ (2004/02/06追記)

古いJavaScriptや、ActionScript(Flashに使われているスクリプト言語)、 MocaScriptなど多くの実装では、各オブジェクトのプロパティとして、 __proto__ が用意されています。これは プロトタイプ の項の"以下細かい話"で「変数objを介して ECMAScript 側からのアクセスは不可能」 として触れた「objのプロトタイプ」にアクセスできるようにする拡張機能です。 obj.__proto__ に値を代入することで、オブジェクト生成後に プロトタイプを変更することも可能になります。

// written for Rhino
var objA = { f: function(){print("objA")} }
var objB = { f: function(){print("objB")} }
var objC = new Object();
var objD = new Object();
var objE = new Object();
objD.__proto__ = objE.__proto__ = objC;

objC.__proto__ = objA; // Cのプロトタイプ変更
objD.f();
objE.f();

objC.__proto__ = objB; // Cのプロトタイプ変更
objD.f();
objE.f();

この __proto__ のようなプロトタイプの連鎖を積極的にプログラミングに取り入れる、 「プロトタイプ指向」というパラダイムが近年地味に流行っています。

argument.caller (2004/02/06追記)

関数のargument.calleeではなく、arugment.caller というプロパティが用意されている処理系も幾つかあります。 (MocaScript、古いJavaScriptなど)これが指すのは、 「今実行中の関数を呼び出した関数の、argumentオブジェクト」。 呼び出し元関数を得たいときには、argument.caller.callee と書きます。

// written for MocaScript
function f1() { f2(); }
function f2() { f3(); }
function f3() { f4(); }
function f4() { f5(); }
function f5() {  g(); }
function g() {
    for(var a=arguments; a; a=a.caller) // 呼び出し元を順にたどる
        if( a.callee == f1 ) // 呼び出され関数がf1なら…?
             write("g was called inside f1");
}

f1();

おわりに (2003/01/04)

…というわけで、この記事はこの辺りでお終いになります。

[連想配列]と[prototype]からなる、ECMAScriptのオブジェクトモデル的な面、 そして、[Functionオブジェクト]と[arguments]、[スコープチェイン] からなる、 関数周りの構造。知らなければ知らないで何も困らない無駄知識ですが、 知っていると知っているで、 このスクリプト言語で色々と面白いプログラムが書けるのではないかなぁと思います。 少しでも読んで頂いた方の参考になっていたら幸いです。

ではでは。

いくつかのさんぷる

presented by k.inaba   under NYSDL.