檜山正幸のキマイラ飼育記 このページをアンテナに追加 RSSフィード Twitter

キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
このブログの更新は、Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama
ところで、アーカイブってけっこう便利ですよ。

2005-08-03 (水)

初心者でも使えてプログラマでも困惑するJavaScript

| 13:04 | 初心者でも使えてプログラマでも困惑するJavaScriptを含むブックマーク

昨日に続いてJavaScriptネタ。

JavaScriptは誰でもすぐ使えるように工夫されている -- これは嘘ではないでしょうが、最初の敷居が低いから「やさしい」とは言えないようです。僕自身と少数の事例からは、C、C++Javaなどの“普通の”言語に慣れていても、JavaScriptには違和感・抵抗感を感じて、食わず嫌いで手を出さないとか、かじったが放り投げるとか、… どうも「むずかしい」みたい。

実行環境が主にブラウザというのがまず異質ですが、この点は置いといて、言語仕様と言語処理系の観点から、JavaScriptの「むずかしさ」(と魅力)を述べたいと思います。

●徹底的にオブジェクト的

JavaScritは、クラス、継承、情報隠蔽機構などを持たないので、まがい物の、あるいは中途半端なオブジェクト指向言語と思われているようです。そもそも、「何がオブジェクト指向か」って議論が不毛だから、別にどうでもいいのだけど、あらゆるところでオブジェクト概念/オブジェクト機構を中核に据<す>えているので、その意味では、むしろ「徹底してオブジェクト的だ」とも言えるでしょう。

ここで、オブジェクトとは、「クラスのインスタンス」とか「データと振る舞いをカプセル化したモノ」とかではなくて、名前の入れ物(つまり、記号空間)であり、含まれる名前には値が付随しています。ですから、動的に変更可能なレコード(構造体)とかJavaのMapのようなものだ、と思ったほうがいいでしょう。

オブジェクトに含まれる名前/値ペアはプロパティと呼びます。ときに、プロパティ名(文字列として実行時にも存在する)のほうを指してプロパティと呼ぶこともあるようです。

言語仕様と実行環境が提供する、既定義(predefined)オブジェクト群が、JavaScriptという言語(の特定方言/特定実装)の実質を構成しています。

●興味深いオブジェクト達

JavaScripには次のようなオブジェクトが存在していることから、「徹底してオブジェクト的」であることが推測されるでしょう。

  • 大域(グローバル)オブジェクト:大域変数や大域関数を保持する。プログラムトップレベルから暗黙的に見えている無名のオブジェクト。
  • 関数オブジェクト:関数コードの実体(テキスト、バイトコード、機械語コードなど)を保持する。引数の定義時個数(アリティ)などの関連情報も保持している。
  • 呼び出し(Call)オブジェクト:関数呼び出し実行のために自動的にアロケートされる。引数、ローカル変数、関数内のthisが指すオブジェクトなどを保持する。

ブラウザなら、大域オブジェクトはWindowオブジェクトです。"window"という名前でWindowオブジェクトを指せますが、これは、大域オブジェクトであるWindowオブジェクトのwindowプロパティが自分自身を指しているのです*1。だから、次はみんな同じです。

<script><!--
 document.write("<p>hello</p>"); //-->
</script>

<script><!--
 window.document.write("<p>hello</p>"); //-->
</script>

<script><!--
 self.document.write("<p>hello</p>"); //-->
</script>

<script><!--
 window.self.window.document.write("<p>hello</p>"); //-->
</script>

●関数オブジェクトと呼び出しオブジェクト

関数がオブジェクトっては面白いですね。関数もデータになるので、昨日やったように、引数として関数を渡すとか、関数を返す関数なんてのが楽勝で書けます。

関数オブジェトの生成は、コンストラクタ関数Functionでも出来ますが、function式(一種のラムダ記法)のほうが便利でしょう。function(n){return n * n;}は二乗する関数オブジェクトを表現します。これを変数に代入することは、関数宣言で関数を定義するのと(ほぼ)同じことです。

var square1 = function(n){return n * n;};

function square2(n) {
 return n * n;
}

js> square1(3)
9
js> square2(3)
9
js> 

関数が実行されるときは、呼び出し(Call)オブジェクトが自動的にアロケートされて、引数やローカル変数を使えるようにします。つまり、名前を持つ引数変数や引数配列arguments、ローカル変数などは呼び出しオブジェクトのプロパティです。

呼び出しオブジェクトは、普通の言語だと、スタック上にアロケートされる活動レコード(activation record)とか関数フレームと呼ばれるものです。JavaScriptではフレームがオブジェクトなのでヒープ上に取られて、リンクリスト管理されるのでしょう。ただし、スコーピングを決定する“スコープチェーン”と、呼び出しスタックに対応する“呼び出しチェーン”は別物で、呼び出し元関数のローカル変数が見えることはありません。これは、僕は良い仕様だと思います(見えてるとバグのもと)。

●クロージャ -- 記憶を持つ関数

フレームに相当する呼び出しオブジェクトがスタックではなくてヒープに取られるので、関数呼び出し終了と同時にローカル環境を解放する必要はありません。呼び出しチェーンからはずしても保存しておくことができます。これがクロージャ(後述)を簡単に実現できる理由です。

function defineConstantFunction(x) {
 return function() {return x;}
}

js> var one = defineConstantFunction(1)
js> one()
1
js> var two = defineConstantFunction(2)
js> two()
2
js> 

上のdefineConstantFunction内で作られる関数オブジェクトは、defineConstantFunction呼び出しのローカル環境(呼び出しオブジェクト)からxの値をもらうようになっています。もし、defineConstantFunction呼び出しの終了でローカル環境が捨てられると、one、twoなどの関数は必要な環境を失いますが、実際は、defineConstantFunctionが終わっても(変数xの値を持った)オブジェクトが保存されて、one、twoなどに環境を提供し続けます*2

関数内部で動的に定義された関数は、親関数のローカル環境を一緒にくっつけた形で(これがクロージャ)保持されるのです。ただし、親(定義もと)関数が抜けた後のローカル環境(のコピーかな?)を保存するので、次のようなイタズラをすると(おそらく)うまくいきません。

function defineConstantFunction(x) {
 var fun = function() {return x;}
 x = 1;
 return fun;
}

●その他

JavaScriptで言う“メソッド”は、(組み込みメソッドを除けば)関数オブジェクトを参照しているプロパティに過ぎません。つまり、メソッドは、プロパティと関数オブジェクトから組み立てられた二次的な概念です。クラスも継承も直接的にはサポートせず、(ちょっと不格好な)二次的な構成物です。

通常のプログラミング言語の常識からは、確かに「変わっている」のですが、少数の(ちょっとエキゾチックな)原則で統制されているのであって、無茶苦茶ではありません。

「なんでもオブジェクト」である点は“オブジェクト的”だし、動作実体が関数だけであり、高階関数も扱える点は“関数的”。が、実際のコードは“手続き的”に書かれます。というわけで、マルチパラダイムというより、パラダイムなんて派閥の枠をコケにしている言語仕様であり、ちょっと小気味よいですね。

*1:オブジェクトグラフはサイクルを持ちます。

*2:高速化のためには、インライン展開して定数埋め込みって手もありますけど。