コア・JavaScript ( JavaScript. The Core. )

この文章は、 Dmitry A. Soshnikov さんの、 ECMAScript に関する優れた記事 "JavaScript. The Core." を許可を得て翻訳したものです。世の中に、 JavaScript のブラウザ API や、実装系に関する記事は多々あれど、 ECMAScript の仕様に則って、ここまで詳しく説明してくれている記事は殆ど無いと思います。今回は翻訳できておりませんが、文中で参照されている Dmitry さんの ES3 シリーズも、読み応えのある( ECMAScript3 の仕様の副読本としても読める)素晴らしい内容ですので、是非チャレンジしてみてください!(ご要望があれば訳します翻訳許可を頂いたので、この記事内で参照されている章から逐次翻訳を進めます!)。

ちなみに Dmitry さんは、計算機科学や数学にも明るい方でらっしゃいます。が、私は違います。極力 Wikipedia などで逐一引くようにはしましたが、そういう面における誤った表現や日本語などありましたら、全て私の責によるものです。もし何かお気づきの点がありましたら、コメントなどでご指摘いただければ幸いです。

はじめに

このメモは、 "ECMA-262-3 in detail" シリーズで得られた知見をまとめたものであり、また概要ともなっています。各々該当する ES3 シリーズ各章へのリファレンスが用意されていますので、もし興味があれば、詳細はそちらを参照してください。

対象読者は、経験を積んだプログラマ、プロフェッショナルです。

それでは早速、 ECMAScript の基本となるオブジェクトの概念から見ていきましょう。


オブジェクト

ECMAScript は、オブジェクトを中心に操作を行う、高度に抽象化されたオブジェクト指向言語です。もちろんプリミティブ値も存在しますが、それらも必要に応じてオブジェクトに変換することが可能です。

"オブジェクト"とは、プロパティと一つのプロトタイプオブジェクトを持つ集合です。プロトタイプは再び "オブジェクト" であるか、または null 値を取ります

ここでは、操作を行うオブジェクトを簡単な図を元に説明します。 ECMAScript3 の仕様では、オブジェクトのプロトタイプは、内部 [[Prototype]] プロパティで参照されますが、この図では "__内部プロパティ名__" という表記を用い、特にプロトタイプオブジェクトについては、"__proto__" と表記します。これは標準仕様ではありませんが、 SpiderMonkey など一部の ECMAScript エンジンでは実際に実装されているプロパティ名でもあります。

コードではこうです。

var foo = {
  x: 10,
  y: 20
};

この場合、このオブジェクト "foo" は、二つの明示的なプロパティと、一つの暗黙のプロパティ __proto__ ( "foo" オブジェクトのプロトタイプへの参照)を持つことになります。



図1. プロトタイプを持った基本的なオブジェクト

では、そもそもこのプロトタイプとは一体何のためにあるのでしょうか?。続いて、この問の答えとなるプロトタイプチェーンへと進みます。


プロトタイプチェーン

プロトタイプオブジェクトとは、それ自身もまたプロトタイプオブジェクトを持つ、一つの単なるオブジェクトのことです。もし、あるプロトタイプオブジェクトが、その "プロトタイプ" への null ではない参照を持つ場合は、さらに同じことが言えます。これがプロトタイプチェーンです。

"プロトタイプチェーン"とは、継承と共有プロパティを実装するために用いられる、オブジェクトの有限の連なりです。

例えばここに、一部ほんの小さな部分だけが異なり、残りの部分は全く同じ二つのオブジェクトがあるとします。明らかに、よくデザインされた設計においては、その同様の機能(コード)をそれぞれのオブジェクトで重複させることなく再利用することを選ぶでしょう。クラスを基本としたシステムにおいては、このコード再利用方法のことをクラスベースの継承と呼ぶことができます。つまり、あるクラス A に重複した機能を持たせ、 クラス B と C はその機能を A から継承し、各々には小さな変更だけを組み込むわけです。

ECMAScript にはクラスという概念はありません。しかし、コードの再利用方法にはさほど違いがなく(むしろ、ある面においてはクラスベースより柔軟な仕組みを持ちます)、これをプロトタイプチェーンを用いて実現しているわけです。このタイプの継承方法を、委譲ベースの継承と呼ぶことができます(より ECMAScript ライクに呼べば、プロトタイプベースの継承ということです)。

クラスベースの例における "A" "B" "C" クラスと同じように、 ECMAScript におけるここでは、 "a" "b" "c" というオブジェクトを用意しましょう。つまり、オブジェクト "a" は、 "b" "c" にまたがる共通部分を持ち、"b" と "c" は各々追加のプロパティやメソッドを持つわけです。

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};
 
var b = {
  y: 20,
  __proto__: a
};
 
var c = {
  y: 30,
  __proto__: a
};
 
// 継承されたメソッドを呼び出します
b.calculate(30); // 60
c.calculate(40); // 80

簡単ですよね?。ここでは、 "b" と "c" オブジェクトが "a" オブジェクトで定義された "calculate" メソッドにアクセスしています。これこそまさに、プロトタイプチェーンを用いて実装されているわけです。ルールは単純です。もしプロパティまたはメソッドがそのオブジェクト自身に見つからなければ(つまりオブジェクトがそのプロパティを自分のモノとして持っていなければ)、次にプロトタイプチェーン上で見つけようとします。さらにそのプロトタイプにおいても目的のプロパティが見つからなかった場合は、プロトタイプのプロトタイプが対象となります。これを、プロトタイプチェーン全体に渡って繰り返すわけです(クラスベースの継承において、継承されたメソッドをクラスチェーン上から探しに行くことと全く同じですね)。そうして、最初に見つかった同名のプロパティ/メソッドが使われることになります。"継承された"プロパティです。しかしもし、プロトタイプチェーン全体を探索しても同名のプロパティが見つからなかった場合は、 undefined 値が返されることになります。

ここでは、"継承された"メソッド内で使われる this オブジェクトが、メソッドが見つかった(メソッドを実際に定義しているプロトタイプ)オブジェクトではなく、"オリジナルの"オブジェクトであることに注意してください。上のコードにおいては、 "calculate" メソッドの内の "this.x" はプロトタイプチェーンの仕組みに基づき "a" オブジェクトのものが用いられるのに対し、"this.y" は、 それぞれ "b" および "c" オブジェクトのものが用いられます。

あるオブジェクトについて、たとえ明示的にプロトタイプが指定されていなくとも、"__proto__" は Object.prototype というデフォルト値をとります。もちろん、 "Object.prototype" オブジェクトも "__proto__" を持ちますが、これはプロトタイプチェーンの末尾であり、 null が代入されています。

次の図が、"a" "b" "c" オブジェクトの継承構造です。



図2. プロトタイプチェーン

さて次に、時として全く同じ、あるいは同様の状態構造(つまり同じプロパティの組み合わせ)を持ちながら、異なる状態値を持たせたい場合があります。この場合には、オブジェクトを特定のパターンの下に生成できるコンストラクタ関数を用いることになります。


コンストラクタ

ある特定のパターンでオブジェクトを生成する他にも、コンストラクタ関数には便利な機能があります。新しく生成されるオブジェクトに、自動的にプロトタイプオブジェクトを設定してくれるのです。このプロトタイプオブジェクトは、 ConstructorFunction.prototype プロパティに格納されます。

例えば、前述の "b" "c" オブジェクトをコンストラクタ関数を用いて書き換えることができます。この場合、 "a" オブジェクト(プロトタイプ)の役割は "Foo.prototype" に当たります。

// コンストラクタ関数
function Foo(y) {
  // 特定のパターンにおいてオブジェクトを生成します。
  // 生成されたオブジェクトは、自身の "y" プロパティを持つことになります。
  this.y = y;
}
 
// 同時に "Foo.prototype" は新たに生成されるオブジェクトの
// プロトタイプへの参照となります。
// つまりここに、前述の例同様に
// 共有/継承プロパティやメソッドを定義できるのです。
 
// 継承されるプロパティ "x"
Foo.prototype.x = 10;
 
// 継承されるメソッド "calculate"
Foo.prototype.calculate = function (z) {
  this.x + this.y + z;
};
 
// そうして、 Foo という "パターン" を用いて、
// "b" "c" オブジェクトを生成します。
var b = new Foo(20);
var c = new Foo(30);
 
// 継承されたメソッドを呼び出します。
b.calculate(30); // 60
c.calculate(40); // 80
 
// 期待通りのプロパティを参照できているか、
// 見てみましょう。
 
console.log(
 
  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true
 
  // "Foo.prototype" は同様に、特別なプロパティ "constructor" を持っています( Foo.prototype.constructor )。
  // これはコンストラクタ関数そのものへの参照です。
  // インスタンス "b" および "c" は、委譲によってこのプロパティを参照し、
  // 自身のコンストラクタをチェックすることができます。
 
  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true
 
  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true
 
);

このコードのオブジェクト同士の関係は、このような図になります。



図3. コンストラクタとオブジェクトの関係

この図は、改めて全てのオブジェクトがプロトタイプを持つことを示しています。コンストラクタ関数 "Foo" でさえ、自身の __proto__ である "Function.prototype" オブジェクトを持ち、そして "Function.prototype" の __proto__ はさらに "Object.prototype" への参照となります。繰り返すと "Foo.prototype" とは、単なる "Foo" の明示的なプロパティの一つであり、"b" 及び "c" オブジェクトのプロトタイプへの参照となっているわけです。

形式的に改めて "クラス化" というコンセプトを見ると(私たちはたった今まさに、 Foo という別の階層への抽象化を行って "クラス化" したわけですが)、コンストラクタ関数とプロトタイプオブジェクトの組み合わせは、 "クラス" と呼ぶことができるかもしれません。実際に、例えば Python の "動的クラスが第一級オブジェクト" であるという概念は、プロパティ/メソッドに関して ECMAScript と全く同じ実装を用いています。この観点から言えば、 Python におけるクラスとは ECMAScript で使われている委譲ベースの継承モデルのシンタックスシュガーであると見ることができます。

このトピックに関するより完全で詳細な説明は、 ES3 シリーズの第7章に見ることができます。そこでは、 "Chapter 7.1 OOP. The general Theory" にてさまざまな OOP の理論やスタイルを ECMAScript のものと比較し、 "Chapter 7.2. OOP. ECMAScript implementation" のすべてを ECMAScript における OOP について割いています。

さて、これまでで基本的なオブジェクトの側面を理解できました。次に、 ECMAScript においてプログラムの実行がどのように実装されているのか見ていきましょう。これは "実行コンテキスト" と呼ばれるものですが、これもまたオブジェクトとして表すことができるのです。そうです、 ECMAScript では、ほとんど全ての操作がオブジェクトという概念の下に行われているわけですから。


実行コンテキストスタック

ECMAScript のコードは3つのタイプに分けられます。 "グローバルコード"、"関数コード"、"eval コード" です。それぞれのコードは、それぞれの実行コンテキストによって評価されます。ECMAScript には、一つのグローバルコンテキストと、多くの関数または eval 実行コンテキストのインスタンスが存在することになります。関数を呼び出すたびに、関数実行コンテキストに入り、 "関数コード" として評価されます。eval を呼び出すたびに、 eval 実行コンテキストに入り、そのコードを "eval コード" として評価するのです。

ここでは、ある一つの関数が無限にコンテキストを生成できるということに注意しておいてください。なぜなら関数の呼び出しというものは、その関数自体への再帰的な呼び出しを含め、内部でさらに新しい関数を呼び出すことで、新しい "コンテキスト状態" を持った実行コンテキストを無限に生成することができるからです(訳注:コンテキストがオブジェクトであることを思い出してください)。

function foo(bar) {}
 
// 同じ関数の呼び出しですが、
// 呼び出しごとに、それぞれ3つの、
// 異なるコンテキスト状態(ここでは、異なる bar の値)を持った
// 実行コンテキストを生成しています。
 
foo(10);
foo(20);
foo(30);

実行コンテキストからは、別のコンテキストを起動できます。例えば関数はまた別の関数を呼び出すことができますし、グローバルコンテキストはグローバル関数を呼び出すことができます。理論上、この連なりはスタックとして実装されています。したがってこれを、実行コンテキストスタックと呼びます。

新しいコンテキストを起動する側のコンテキストを、 "caller" 、起動される側のコンテキストを "callee" と呼びます。グローバルコンテキストから関数が呼び出され、その関数がまた内部で関数を呼び出すことができるように、"callee" は同時にまた新たな "callee" の "caller" になることができるわけです。

caller が callee を起動するとき、 caller はその実行を一旦停止し、制御フローを callee に渡します。つまり、 callee はスタックに積み込まれ、現行の(アクティブな)実行コンテキストとなります。callee となったコンテキストが終了すると、制御は caller に戻ります。そうして、 caller のコンテキストの評価が継続してゆき、また新たなコンテキストを起動しながら、末端まで評価を進めていくこととなります。callee は、単純に return または例外によって終了できます。投げられたものの caller でキャッチされない例外は、スタック上の残りのコンテキストを終了( pop )させます。

要するに、 ECMAScript の全てのプログラム実行は、実行コンテキスト( Execution Context = EC )のスタックとして表現できるわけです。もちろん、スタックの最上にあるものがアクティブなコンテキストです



図4. 実行コンテキストスタック

プログラムが開始されると同時に、グローバル実行コンテキストに入ります。スタックの最初、かつ最下のコンテキスト要素です。グローバルコードはまず、さまざまな初期化や、オブジェクト、関数を生成します。そしてグローバルコンテキストの実行の間、コードはまた別の(生成された)関数を起動してゆき、それぞれの実行コンテキストへと入っていきます。つまり、スタックに新しいコンテキスト要素が push されていくのです。そうしてグローバルコードの実行が完了すると、ランタイムはユーザのマウスクリックなど、イベントの発生を待つ状態へと移ります。イベントが発生次第、関数が起動され、また新たなコンテキストへと入っていきます。

この図は、 "Global EC" というグローバルコンテキストが "EC1" という関数コンテキストに入り、そして抜けだして行く際のスタックの変化を表したものです。



図5. 実行コンテキストスタックの変化

このようにして ECMAScript のランタイムはコードの実行を管理しているのです。

ECMAScript の実行コンテキストに関するより詳しい内容については、 "Chapter 1. Execution context" に見ることができます。

そして、繰り返し言うように、スタック上の全ての実行コンテキストはオブジェクトとして表現できます。それでは、このオブジェクトの構造と、コードが実行されるためにどんな状態(プロパティ)が必要なのか、見ていくことにしましょう。


実行コンテキスト

実行コンテキストは抽象的にはごくごく単純なオブジェクトとして表現でき、全ての実行コンテキストは、"コンテキストの状態"とでも呼ぶべき、そのコンテキストが属するコードの実行状態を追跡するためのプロパティを持っています。次の図を見てください。



図6. 実行コンテキストの構造

これら三つの必須プロパティ( Variable Object/変数オブジェクト、Scope Chain/スコープチェーン、thisValue/this値)の他にも、実装によってその他の状態を保持する場合がありますが、ここではこれら3つの重要なプロパティについて、詳細を見ていくことにしましょう。


変数オブジェクト( Variable Object )

変数オブジェクトは、その実行コンテキストに関わるデータの "スコープ" であり、そのコンテキスト内で定義された変数や関数を保持する特別なオブジェクトです。

※関数定義ではなく、関数式( Function Expression )は変数オブジェクトには含まれません。

変数オブジェクトとは抽象的なコンセプトであり、役割です。異なるコンテキストにおいては、実際には、異なるオブジェクトが変数オブジェクトとしてふるまいます。例えば、グローバルコンテキストにおける変数オブジェクトは、そのままグローバルオブジェクトそのものです(訳注:ブラウザ実装においては window オブジェクト)。このために、私たちはグローバルオブジェクトのプロパティを通じて、グローバル変数にアクセスすることが可能になっています。

グローバル実行コンテキストにおける次の例を見てください。

var foo = 10;
 
function bar() {} // 関数定義( function declaration, FD )
(function baz() {}); // 関数式( function expression, FE )
 
console.log(
  this.foo == foo, // true
  window.bar == bar // true
);
 
console.log(baz); // ReferenceError, "baz" is not defined

すなわち、このグローバルコンテキストの変数オブジェクト( VO )は下記のようなプロパティを持つと言えます。



図7. グローバル変数オブジェクト

※関数式である関数 "baz" は、変数オブジェクトには含まれていません。ですから、 "baz" 関数の外側からのアクセスでは、 ReferenceError となります。

注意していただきたいのは、 C や C++ など他の言語と異なり、 ECMAScript では関数のみが新しいスコープを生成します。変数やある関数のスコープ内で定義された内部関数は、その関数の外側から直接はアクセスすることができませんし、グローバル変数オブジェクトを汚染することもありません。

eval を呼び出すと、新しい eval 実行コンテキストに入ることになります。ただし、 eval 実行コンテキストは、変数オブジェクトとしてグローバル変数オブジェクト、または eval を呼び出した関数である caller 実行コンテキストの変数オブジェクトをそのまま用います。

それでは、関数コンテキストの場合の変数オブジェクトはどうなるのでしょう?。関数コンテキストにおいては、変数オブジェクトの役割を果たすものとして、起動オブジェクト( Activation Object )が与えられます。


起動オブジェクト( Activation Object )

関数が caller 実行コンテキストによって起動(呼び出し)されるとき、起動オブジェクトと呼ばれる特別なオブジェクトが生成されます。起動オブジェクトには、関数の仮引数や、 arguments という特別なオブジェクト(仮引数のマップですがインデックスアクセス可能なオブジェクト)もプロパティとして積み込まれます。その後、この起動オブジェクトは、関数コンテキストにおける変数オブジェクトとして使われるのです。

簡単に言い換えると、関数の変数オブジェクトとは、変数や関数定義の他に関数の仮引数と arguments オブジェクトを格納した単純な変数オブジェクトであり、これを起動オブジェクトと(訳注:仕様上)わざわざ呼ぶわけです。

次の例を見てください。

function foo(x, y) {
  var z = 30;
  function bar() {} // FD (関数定義)
  (function baz() {}); // FE (関数式)
}
 
foo(10, 20);

この場合は、図のような "foo" 関数コンテキストに対する起動オブジェクトが生成されていると考えられます。



図8. 起動オブジェクト( Activation Object )

※繰り返しになりますが、関数式 "baz" は変数オブジェクト(この場合起動オブジェクト)には含まれません。

その他、さまざまケース(変数や関数定義の "巻き上げ" など)に関するより完全な説明は、 "Chapter 2. Variable object" を参照してください。

さて、それでは、次のステップへと進みましょう。周知の通り、 ECMAScript では、内部関数からその親の関数スコープ内の変数や、グローバルコンテキストの変数にアクセスすることができますね。これまで見てきた変数オブジェクトをコンテキストの "スコープオブジェクト" として見つめ直してみれば、プロトタイプチェーン同様に、スコープチェーンが存在し、これを実現していると見ることができるのです。


スコープチェーン

スコープチェーンとは、コンテキストのコード中に出現した識別子を探索するための、オブジェクトの連なりです。

ルールはまたしても単純で、プロトタイプチェーンと同様です。ある変数がそのスコープ(つまりその変数/起動オブジェクト)内に見つからなかった場合、親の変数オブジェクトへと探索が続いていきます。

コンテキストの観点から見れば、識別子とは変数、関数定義、仮引数などの名前に相当します。関数がそのコード中でローカル変数(またはローカル関数や仮引数)として存在しない識別子を参照するとき、そうした変数は自由変数と呼ばれます。この自由変数を探索するためにこそ、スコープチェーンが用いられるのです。

一般的なケースでは、スコープチェーンとは "親(そのまた親そのまた…)のリスト" にあたる "変数オブジェクトのリスト" に、(先頭として)その関数自体の変数/起動オブジェクトを加えた( push した)結果のリストのことです。ただし、 "with オブジェクト" や try-catch 節の特別なオブジェクトのように、スコープチェーンには、コンテキストの実行中に動的にオブジェクトが追加される場合があることは覚えておいてください。

識別子を解決(上方探索)するとき、スコープチェーンはまず起動オブジェクトから探索され、識別子がその起動オブジェクト中に見つからない場合、スコープチェーンの頂上まで探索されます。繰り返しますが、これはプロトタイプチェーンと一緒です。

var x = 10;
 
(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" と "y" は "自由変数" であり、
    // bar の持つスコープチェーン中の、
    // "次と次の" ( bar の起動オブジェクトの後の)
    // オブジェクトにて見つかります。
    console.log(x + y + z);
  })();
})();

スコープチェーン間の関係性は、チェーンの次のオブジェクトを参照する暗黙の __parent__ プロパティを用いて考えてみることができます。このアプローチは実際の Rhino 用コードで試すことができ、この手法はまさに ES5 のレキシカル環境において( "outer link" という名前で)用いられています。スコープチェーンのその他の表現方法としては単純な配列が考えられるでしょう(訳注:スコープチェーンを配列として表現した際の説明は拙記事を参照ください)。__parent__ の考え方を用いれば、上記のコード例は下記の図で表すことができます(親の変数オブジェクト達は、 bar 関数の [[Scope]] 内部プロパティに保存されています)



図9. スコープチェーン

※少しだけ前述しましたが、 with文とcatch節によってコードの実行時にスコープチェーンが拡張される場合があります。

思い出していただきたいのは、これらの変数オブジェクトもまた単純なオブジェクトであるということです。つまり、これらはプロトタイプを持ち、プロトタイプチェーンを持つのです。この事実により、スコープチェーン探索は二次元で行われることになります。(1)まずスコープチェーンを辿る次元。 (2)次に全てのスコープにおけるプロトタイプチェーンを辿る次元です(もしそのスコープオブジェクト=変数オブジェクトがプロトタイプを持つ場合)。

例を見ていただく方が速いでしょう。

Object.prototype.x = 10;
 
var w = 20;
var y = 30;

// SpiderMonkey では、グローバルオブジェクト
// (すなわちグローバルコンテキストの変数オブジェクト)
// は "Object.prototype" を継承しています。
// そのため、
// グローバル変数としては定義されていない "x" を
// プロトタイプチェーン中から、見つけることができます。
 
console.log(x); // 10
 
(function foo() {
 
  // "foo" のローカル変数
  var w = 40;
  var x = 100;
 
  // "x" は "Object.prototype" オブジェクトから解決されます。
  // {z: 50} というオブジェクトが "Object.prototype" を継承しているためです。
  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }
 
  // with オブジェクトがスコープチェーンから削除されました。
  // 結果、 "x" は再び、 "w" 同様
  // "foo" コンテキストの変数オブジェクトから
  // ローカル変数として解決されるようになります。
  console.log(x, w); // 100, 40

  // ブラウザ実行環境では、
  // 通常このようにして、ローカル変数によって隠された
  // グローバル変数にアクセスすることができます。 
  console.log(window.w); // 20
 
})();

図に表すとこのような構造です。(思い出してください。順序としては __parent__ リンクを辿る前に、 __proto__ リンクが探索されます)



図10. "withで拡張された" スコープチェーン

※全ての実装において、グローバルオブジェクトが "Object.prototype" を継承しているわけではありません。この図のような振る舞い(グローバルコンテキストから "定義されていない" 変数 "x" を参照する)は、例えば SpiderMonkey でテストできます。

親となる全ての変数オブジェクトが存在している限り、内部関数から親のデータを取得することに、何ら特別なことはありません。単に、必要な変数に関してスコープチェーンを探索するだけです。では、前述したように、コンテキストが終了し、全ての状態やコンテキストそのものが破壊された場合はどうなるのでしょうか。その時、内部関数が親の関数によって返されていたとしたら?しかもこの返された関数は、後に別のコンテキストによって起動され得るのです。解決され得た自由変数のコンテキストが、既に消えてしまっていたとしたら、そうした関数呼び出しは一体どうなるのでしょうか?一般的には、こうした問題の解決を助けてくれるものを、(レキシカル)クロージャと呼びます。 ECMAScript では、これが直接スコープチェーンの考え方に関連してきます。


クロージャ

ECMAScript では、関数は "第一級" オブジェクトです。この言葉の意味するところは、つまり、関数を他の関数の実引数として渡すことができるということです(この場合渡される関数は "functional arguments/関数型の引数" 略して "funargs" と呼ばれます)。"funargs" を受け取る関数は、高階関数、またはより数学的に表現すれば、作用素と呼ばれます。また、同時に関数は他の関数から戻り値として返すことができます。他の関数を返す関数のことを、 "function valued functions/関数型の関数"、または関数値を取る関数と呼びます。

"funarg" 及び "functional values" には、2つの理論上の問題が知られており、これら2つは、 "funarg 問題" または "functional argument に関する問題" としてまとめられていますが、まさに、この "funarg 問題" を解決するためにこそ、クロージャの考えは生み出されたのです。

それでは、これら2つの問題について詳細を見ていきましょう( ECMAScript においては、関数の [[Scope]] 内部プロパティの図に示されるようにこれらの問題は見事に解決されるのですから!)

"funarg 問題" の最初の一つは、 "上方/上向きの funarg 問題”です。これは、関数が他の関数から上に(外側に)戻されるとき、自由変数を参照している際に起こります。親のコンテキストが終了した後にも親のコンテキストの変数を参照できるよう、内部関数の生成時にその関数の [[Scope]] プロパティに親のスコープチェーンを保存しておきます。そして関数が呼び出された際には、その関数のコンテキストが、起動オブジェクトと(予め保存しておいた) [[Scope]] プロパティによって生成されるのです(図6)。

スコープチェーン = 起動オブジェクト + [[Scope]]

もう一度重要なポイントを覚えておいてください。まさに生成されるその時に、関数は親のスコープチェーンを(自分の [[Scope]] として)保存します。この保存されたスコープチェーンが、いずれ関数が呼び出された際に、変数探索の対象として使われることになるのです。

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" は関数を返しますが、
// 返された関数は "x" という自由変数にアクセスしています。
 
var returnedFunction = foo();
 
// グローバル変数 "x" を設定します
var x = 20;
 
// 返された関数を実行します。
returnedFunction(); // 20 ではなく、 10 となります

このスタイルのスコープを、静的またはレキシカルスコープと呼びます。変数 "x" は、返された関数 "bar" の保存された [[Scope]] の中から解決されます。一般的には、上記の例の "x" が 10 ではなく 20 となる動的スコープというものも存在します。しかし、動的スコープは ECMAScript では採用されていません。

"funarg 問題" の2つ目は、 "下方/下向きの funarg 問題" です。このケースでは、親のコンテキストは存在しても、識別子を解決する対象があいまいになってしまっています。問題はどの親スコープの識別子の値が使われるべきか。ということになります。関数生成時に静的に保存されたものでしょうか?あるいは、実行時に動的に生成されたもの( caller をスコープとする)のでしょうか?この曖昧さを避けるためにも、静的スコープが使われることになっています。

// グローバル "x"
var x = 10;
 
// グローバル関数 "foo" 
function foo() {
  console.log(x);
}
 
(function (funArg) {
 
  // ローカル "x"
  var x = 20;
 
  // グローバルの "x" が呼び出されることは
  // 明白です。
  // なぜなら静的に保存された "foo" の [[Scope]] 
  // に存在する "x" はグローバルの "x" であり、
  // funArg を起動した caller スコープの
  // ローカル "x" ではないからです。

  funArg(); // 20 ではなく 10
 
})(foo); // "foo" を "funarg" として下向きに渡す

つまり静的スコープは、ある言語がクロージャ機能を持つための必要条件であるということです。しかしいくつかの言語では、動的と静的スコープの両方を組み合わせて持ち、(内部ブロックをクロージャとするか、しないかを)プログラマが自由に選ぶことができる場合があります。ECMAScript では、静的スコープのみが採用されていますから( "funarg 問題" は解決されているということです)、結論はこうです。ECMAScriptクロージャを完全にサポートし、技術的には [[Scope]] 内部プロパティをもって実装しているのです。クロージャに正確な定義を与えてあげましょう。

クロージャは、コードブロック( ECMAScript においては関数)の組み合わせであり、静的またはレキシカルに全ての親スコープを保存する。しかるに、これらの保存したスコープを経由して、関数は簡易に自由変数を解決することができる。

※全ての(通常の)関数は [[Scope]] 内部プロパティを生成時に保存するため、理論上 ECMAScript の全ての関数はクロージャであると言えます。

もうひとつの重要なことは、いくつかの関数は同じ親スコープを持ち得るということです(例えば二つの内部関数があれば、全く普通のことです)。この場合、そのぞれの関数の [[Scope]] プロパティに保存された変数は、同じ親スコープチェーンを持つ関数間で共有されることになります。
つまり、あるクロージャからの共有変数への変更は、この変数を参照するその他全てのクロージャに影響するということです。

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}
 
var closures = baz();
 
console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

このコードを図に表すと下図のようになります。



図11. 共有される [[Scope]]

この機能はループにおけるいくつかの戸惑い易い問題を説明してくれるでしょう。ループ内部で生成された関数でループカウンタを使用すると、時折、全ての関数内でカウンタが同じ値を示すという意図しない結果となってしまう場合があります。今は、この理由ははっきりしていることでしょう。なぜなら、全ての関数は同じ [[Scope]] を共有しており、そこには最後に代入されたループカウンタの値が格納されているからです。

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}
 
data[0](); // 0 ではなく 3
data[1](); // 1 ではなく 3
data[2](); // 2 ではなく 3

この問題を解決するにはいくつかのテクニックがあります。一つは、スコープチェーンにもう一つオブジェクトを追加してあげる方法です。例えば追加の関数を利用して…、

var data = [];
 
for (var k = 0; k < 3; k++) 
  // 無名内部関数の変数オブジェクトが
  // [[Scope]] の先頭に追加される。
  // ループカウンタは共有された "k" からではなく
  // 自分自身のみが持った "x" から参照できる。
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // "k" の値を渡す
}
 
// 今度は正解
data[0](); // 0
data[1](); // 1
data[2](); // 2

クロージャとその実際上の応用についてより興味がある方は、是非 "Chapter 6. Closures" をご覧ください。スコープチェーンについては、まさにその名のとおりの "Chapter 4. Scope chain" という章があります。

それでは次の項に移りましょう。実行コンテキストの最後のプロパティ、this という値についての考察です。


"this"

"this" は実行コンテキストに関連した特別なオブジェクトです。したがって、コンテキストオブジェクトと名付けることができます(実行コンテキストが起動されるコンテキストのオブジェクト、という意味です)(訳注:実行コンテキストをオブジェクトとして表したもの(図6)とは区別してください)。

どんなオブジェクトでも、あるコンテキストの "this" になることができます。私はここで再度、 ECMAScript の実行コンテキストに関する誤解、特に "this" について明確にしたいと思います。しばしば、 "this" は、誤って変数オブジェクトのプロパティであると説明されることがあります。最近の誤りは、例えばこの本です(もちろん、この章自体はとても素晴らしいものなのですが)。もう一度説明します。

"this" は実行コンテキストのプロパティであり、変数オブジェクトのプロパティではありません。

この特性はとても重要なポイントです。なぜなら変数と異なり、 "this" は決して識別子解決プロセスには参加しないからです。つまり、コード中で "this" にアクセスするとき、その値は直接実行コンテキストから参照されており、スコープチェーン探索は行われていません。 "this" の値は、コンテキストに入ったその瞬間に一度だけ、決定的に決定されるのです。

ところで、また Python の話になりますが、 ECMAScript に対し Python では、メソッドは "self" という仮引数を単なる変数として受け取り、変数と同様に識別子解決され、さらには実行中に別の値を代入することさえできます。ECMAScript では、これはできません。 "this" に新しい値を代入することはできないのです。なぜなら、繰り返しになりますが、 "this" は変数ではなく、変数オブジェクトにも格納されていないからです。

グローバルコンテキストでは、 "this" はグローバルオブジェクトそのものです(つまり、 "this" が変数オブジェクトに等しい、ということになります)。

var x = 10;
 
console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

関数コンテキストでは、全ての関数呼び出しにおいて "this" オブジェクトは異なる場合があります。呼び出し式の形(単純に () カッコを使って関数を起動する方法など)をもって caller から呼び出される際に、 "this" の値が関数コンテキストに与えられます。例えば、以下の関数 "foo" は callee であり、 caller であるグローバルコンテキストから呼び出されています。下記のコードでは、同じ関数のコードでも、異なる関数呼び出し(異なる関数起動方法)によって、それぞれ異なる "this" 値が caller から与えられていることに注目してください。

// "foo" 関数のコードは一切変更されません。
// しかし、異なる関数呼び出しによって
// "this" の値が異なってきます。
 
function foo() {
  alert(this);
}
 
// caller が "foo"( callee ) を起動し、
// callee に "this" を与えています
 
foo(); // グローバルオブジェクト
foo.prototype.constructor(); // foo.prototype
 
var bar = {
  baz: foo
};
 
bar.baz(); // bar
 
(bar.baz)(); // これも bar
(bar.baz = bar.baz)(); // しかしここでは global object
(bar.baz, bar.baz)(); // これも global object
(false || bar.baz)(); // これも global object
 
var otherFoo = bar.baz;
otherFoo(); // さらにこれも global object

それぞれの関数呼び出しにおいて、なぜ(そしてもっと重要な…どうやって) "this" の値が変わっているかについての詳細は、 "Chapter 3. This"を参照してください。上記の例全ての詳細を説明しています。


おわりに

これで、簡単な概要を終わります。とはいえ、そんなに簡単ではありませんでしたね。しかし、これらのトピックを全て詳細に説明するには、一冊の本が丸々必要になってしまいますし、結局2つの大きなトピックには触れずじまいでした。関数(関数の種類、関数定義と関数式の違いなど)と、 ECMAScript における式の評価方法についてです。これらはそれぞれ、 ES3 シリーズの "Chapter 5. Functions""Chapter 8. Evaluation strategy" で触れています。

コメント、質問や不足があれば、是非とも遠慮無く、コメント欄におねがいします。

ECMAScript 習得の旅の、幸運をお祈りします!

著: Dmitry A. Soshnikov
翻訳原文最終更新日: 2010-09-02