mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

詳細 ECMA-262-3 第8章 評価戦略

全国20人の ECMA セオリストのみなさま、おつかれさまです。大形尚弘です。

ついに Dmitry 先生の ES3 シリーズも最終章となりました。この後に ES5 シリーズが5章続きますが、それらは基本的に今シリーズの補足として書かれたものですので、ここまでお読みいただいたみなさまは、ほぼ ECMAScript の理論的側面を理解したと言えます。

もしそうでない部分があったとしても、実際に ECMAScript の仕様書をご覧いただければ、これまでとは全く理解度が違っていて、あっという間に足りない知識を補足できると思います。端的に、「仕様が読める」ようになっているはずです。

ES5 であれば、本来 PDF である仕様書を、有志の方が es5.github.com にて「注釈付きの」 HTML 形式で公開し、頻繁に更新されています。注釈の一つはもちろん我らが Dmitry 先生の ES シリーズです。ここまでお読みいただいたみなさまには、仕様書で初めて目にする言葉や概念はほんの少しです。是非、おすすめします。

それでは ES3 シリーズ最終章『評価戦略』をどうぞ。またいずれ ES5 シリーズの翻訳でお会いできればと思います。

第8章 評価戦略

目次

  1. はじめに
  2. 概説
    1. 値渡し
    2. 参照渡し
    3. 共有渡し
      1. 値渡しの特別なケースとしての共有渡し
      2. 共有渡しとポインタ
  3. ECMAScript での実装
    1. 各用語について
  4. 結論
  5. 参考文献

はじめに

この短い章では、 ECMAScript における、関数に引き渡す引数の戦略について検討します。

一般的に、コンピュータサイエンスにおけるこの部分は、評価戦略と呼ばれます。つまり、プログラミング言語においてあるを評価し、演算するための規則の集合です。関数に引き渡す引数の戦略とは、その特定のケースです。

この章の動機は、あるフォーラムにおける同様のトピックでした。そこでの議論の結果私たちは、 ECMAScript における引数の引き渡し戦略について、ほぼ完全な表現を記述するに至りました。フォーラムや議論の中で用いることのできる、相応の定義も与えることになったのです。

多くのプログラマは、 JavaScript (あるいは常用する言語においても同様)におけるオブジェクトは参照によって関数に与えられ、一方プリミティブ型の値については値によって与えられると強く考えています。さらに、こうした「事実」はさまざまな記事、フォーラムでの議論、さらには JavaScript に関する書籍においても見ることができます。しかしながら、この用語の使い方がどれほど正確で、この説明が本当に正しいものなのか(さらに重要なことは、この理解がどれだけ正しいものなのか)、この章で議論していきたいと思います。

概説

簡単に申し上げると、一般的に評価戦略には二つの種類があります。正格、つまり実引数が関数への適用の前に計算されるものと、非正格、原則的に実引数の計算は必要になったときに都度実行されるもの(いわゆる「遅延」計算です)があります。

しかしここでは、私たちは ECMAScript の観点から、その理解に重要な、関数への引数の引き渡しに関する基本的な戦略について検討します。

始めに、 ECMAScript では、そのほかの多くの言語と同じように(例えば C 、 Java 、 Python 、 Ruby 等)、正格な引数引き渡し戦略が用いられています。

また、引数が評価される順序も重要です。 ECMAScript では左から右です。他の言語や実装では、この逆の評価順序(右から左)も使われる場合があります。

正格な引き渡し戦略はまた、その中でいくつかの戦略に細かく分けられています。そのうち最も重要ないくつかについては、この後で議論していきます。

以下で述べる全ての戦略が ECMAScript で採用されているわけではありませんので、例えば論理的な戦略を具体的に説明するために、 Pascal 様のシンタックスの抽象的な擬似コードを用いることにします。

値渡し

この種の戦略は、多くのプログラマに良くなじみのあるものです。ここでは、実引数の値は呼び出し元によって渡される対象の値の複製です。関数内部での仮引数への変更は、渡された対象の外側での状態には影響しません。一般的に、外部の対象が複製される際には新しいメモリ領域の割り当てが行われ(この議論では具体的な実装はさほど重要ではありません。スタックでも、動的メモリでもかまいません)、まさにこのメモリ上の新しい領域の値が、関数の内部で使われます。

bar = 10
 
procedure foo(barArg):
  barArg = 20;
end
 
foo(bar)
 
// foo 内部の変更は、
// 外側の bar には影響しない
print(bar) // 10

しかしながらこの戦略は、関数への実引数がプリミティブ値で無く、複雑な構造、オブジェクトである場合に、大きなパフォーマンスの問題を抱えてしまいます。これはまさに、 C/C++ で構造体が関数に値渡しされるとき、完全に複製されるときに起こっていることです。

以下の各評価戦略の説明で用いる、一般的なコード例をここに載せておきます。ある抽象的な手続きが、二つの引数を受け取ります。一つはオブジェクトの値、もう一つはブーリアンフラグで、このフラグによってオブジェクトの値を完全に(新しい値を代入して)変更するか、単にオブジェクトのプロパティを変更するのかを決定します。

bar = {
  x: 10,
  y: 20
}
 
procedure foo(barArg, isFullChange):
 
  if isFullChange:
    barArg = {z: 1, q: 2}
    exit
  end
 
  barArg.x = 100
  barArg.y = 200
 
end
 
foo(bar)
 
// 値渡しの戦略では、
// 外側のオブジェクトは変更されない
print(bar) // {x: 10, y: 20}
 
// 全体の変更(新しい値の代入)
// においても同様
foo(bar, true)
 
// 何の変更もされていない
print(bar) // {x: 10, y: 20} 、 {z: 1, q: 2} ではない

参照渡し

次に、参照渡しの評価戦略は(これもまたよく知られたものです)、値の複製ではなく、対象への暗黙の参照を受け取ります。つまり、外側から渡される対象の、直接のメモリアドレスです。関数の内側での仮引数への変更は(新しい値の代入であれ、プロパティの変更であれ)、全て外側の対象にも影響します。なぜなら、まさにこの対象のアドレスが仮引数に結びついているからです。つまり、この場合の実引数とは、外側から渡される対象のエイリアスのようなものなのです。

擬似コードです。

// 手続き foo の定義は前の例を参照
 
// 同じオブジェクトを用意して
bar = {
  x: 10,
  y: 20
}
 
// 参照渡しの場合の
// 手続き foo の結果は
// 次の通り
 
foo(bar)
 
// オブジェクトのプロパティ値は変更されている
print(bar) // {x: 100, y: 200}
 
// 新しい値の代入も
// オブジェクトに影響する
foo(bar, true)
 
// bar は新しいオブジェクトを参照している
print(bar) // {z: 1, q: 2}

この戦略では、複雑なオブジェクトを効率的に渡すことができます。例えば、非常に大量のプロパティを持った構造体を渡す場合などに有効です。

共有渡し

最初の二つの戦略が多くのプログラマのよく知るところだったとしても、この戦略(あるいはより正確には、ここで議論する用語)は、それほど広く認知されていません。しかし、すぐに分かるとおり、まさにこれが、 ECMAScript での引数渡し戦略において重要な役割を果たしています。

この戦略の別の呼び方としては、「オブジェクト渡し」や「オブジェクト共有渡し」などがあります。

「共有渡し」の戦略は、 Barbara Liskov によって、 CLU プログラミング言語のために1974年に提案され、名付けられました。

この戦略の中心となるポイントは、関数は対象への参照の複製を受け取るという点です。この参照の複製が仮引数に結びつき、その値となるのです。

この場合、参照という考え方が現れてはいますが、参照渡しとして扱われるべきではありません(多くの人が誤解しているポイントです)。なぜなら実引数の値は直接のエイリアス(外側の対象のアドレスそのもの)ではなくアドレスの複製だからです。

参照渡しとの大きな違いは、関数内部での仮引数への新しい値の代入は、外側の対象には影響しない(参照渡しの場合は影響します)ということです。しかしながら仮引数はアドレスの複製を保持しており、外側にあるものと同じ対象にアクセスすることが可能ですので(つまり外側から渡された対象が、値渡しのように完全に複製されているわけではない)、関数ローカルの仮引数のプロパティへの変更は、外側のオブジェクトに影響します

// 手続き foo の定義は前の例を参照
 
// もう一度、同じオブジェクトを用意し
bar = {
  x: 10,
  y: 20
}
 
// 共有渡しの場合、
// 次の通り
// オブジェクトに影響する
 
foo(bar)
 
// オブジェクトのプロパティ値は変更されている
print(bar) // {x: 100, y: 200}
 
// しかしオブジェクトの完全な変更は
// 反映されない
foo(bar, true)
 
// 依然として一つ前の関数呼び出しの前と同じ状態
print(bar) // {x: 100, y: 200}

この戦略は、言語の大部分において、プリミティブ値ではなくオブジェクトを対象に演算を行うということを前提にしています。

値渡しの特別なケースとしての共有渡し

共有渡しの戦略は多くの言語で採用されています。例えば Java 、 ECMAScript 、 Python 、 Ruby 、 Visual Basic 等です。

Python コミュニティではまさにこの用語、共有渡しが使われています。他の言語に関してはまた別の用語が用いられたりしますが、命名上他の戦略と混同される原因ともなっています。

多くの場合、例えば Java や ECMAScript 、 Visual Basic においては、この戦略は参照の複製という値を意味する、値渡しの特別なケースとも呼ばれます。

この場合、関数の内部において、仮引数の名前に新しい(アドレス)を束縛しても、外側の対象には影響を及ぼしません。

こうした用語の交錯は、実に(この問題を深く検証してみなければ)誤った取り扱い( JavaScript において関数にどのようにオブジェクトが渡されるのかというフォーラム中の議論)につながります。

しかし理論的には、これはまさに特定の値、つまりアドレスの複製を扱う値渡しの特別なケースであり、用語の規則を破っているわけでは無いのです。

Ruby では、この戦略が参照渡しと名付けられています。しかしここでも、(値渡しのように)大きな構造体の複製が渡されるわけではなく、対象へのオリジナルの参照を取り扱うわけでもないのです。結果として、こうした用語の錯綜が混乱につながってしまいます。

この理論は、参照渡しの特殊なケースを説明するものではなく、値渡しのある特定のケースを説明するものです

しかしながら、こうした技術( Java 、 ECMAScript 、 Python 、 Ruby 等)で、それぞれに独自の用語が用いられていることは認識しておく必要があるでしょう。ただし実際に、それらは全て、理論的には共有渡しと名付けられるものです。

共有渡しとポインタ

C/C++ に関して言うと、この戦略は論理的にポインタ渡しに似ていますが、一点大きな違いがありますポインタ渡しの場合、ポインタをデリファレンスし、対象を完全に変更することができるのです。しかし一般的に、値(アドレス)をポインタに代入することは、新しいメモリブロックにポインタを束縛するということです(ポインタがその代入以前に参照していたメモリブロックはそのまま変更されず残ります)。そしてポインタを経由して参照する対象のプロパティの変更は、外側の対象にも影響するのです。

したがって、ポインタを用いて表現するなら、共有渡しとはアドレスの値渡しであり、まさにこのアドレスとはポインタがそれに当たります。この場合、共有渡しは代入においてポインタ(しかしデリファレンスできない)のようにふるまい、プロパティ変更においては参照(デリファレンス操作を必要としない)のようにふるまうある種の「シンタックスシュガー」であると言えます。時折これは「安全なポインタ」とも呼ばれます。

しかし、 C/C++ はまた、対象のプロパティを、明確なポインタのデリファレンス無しに参照できる、特別な「シンタックスシュガー」を用意しています。

(*obj).x ではなく obj->x

より C++ に近づいて考えてみると、この考え方は「スマートポインタ」の実装の一つに見ることができます。例えば boost::shared_ptr は、この用途のために代入演算子とコピーコンストラクタをオーバーロードし、対象への参照カウントを使い、 GC によって対象を削除します。このデータ型は、まるでこの戦略と同様の名前、shared_ptr を持ちます。

ECMAScript での実装

さて、これまでの通り、私たちは ECMAScript で用いられている、ある対象を引数として関数に引き渡す評価戦略について知ることができました。共有渡しです。引数のプロパティの変更は外側に影響し、新しい値の代入は、外側の対象には影響しません。

しかし前述したように、 ECMAScript デベロッパの中では、この戦略に対するローカルな用語も使われています。それは「値が参照の複製である場合の値渡し」と呼ばれるものでした。

JavaScript の発明者である Brendan Eich もまた、参照の複製(アドレスの複製)が渡されると述べています。あるフォーラムの議論の中には、 ECMAScript の開発者がこれを値渡しと呼んでいる場面もありました

より正確に言えば、このふるまいは単純な代入を検討することで理解できます。二つの異なるオブジェクトがあり、しかしそれらは同じ値、つまり同じアドレスの複製をそれぞれが持つという場合です。

ECMAScript のコードで見てみましょう。

var foo = {x: 10, y: 20};
var bar = foo;
 
alert(bar === foo); // true
 
bar.x = 100;
bar.y = 200;
 
alert([foo.x, foo.y]); // [100, 200]

つまり、二つの識別子(名前束縛)が、メモリ上の同じ対象に束縛されているということです。対象を共有しているのです。

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) <= bar value: addr(0xFF)

そして、代入は単に識別子を新しい対象(新しいアドレス)に束縛し、(それ以前に)束縛されていた対象(古いアドレス)には影響しません。これがもし参照の場合では(メモリアドレスの参照先そのもののを書き換えますので)、影響してしまいます。

bar = {z: 1, q: 2};
 
alert([foo.x, foo.y]); // [100, 200] - 何も変わらない
alert([bar.z, bar.q]); // [1, 2] - しかし bar は新しい対象を参照する

こうして foobar は異なる値、異なるアドレスを持つことになります。

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF)
bar value: addr(0xFA) => {z: 1, q: 2} (address 0xFA)

繰り返しになりますが、こうした一連の手続きは全て、オブジェクト型の場合の変数値はアドレスでありオブジェクト構造そのものではないという事実によるものです。ある変数を他の変数に代入することは、その値参照を複製することであり、両方の変数はメモリ上の同じ場所を参照します。次に新しい値(新しいアドレス)の代入が行われると、これは古いアドレスから名前を解放し、新しいアドレスに束縛します。まさにこれが、重要な、参照渡し戦略との違いです。

さらに踏み込んで考えると、 ECMA-262 標準で与えられる抽象レベルのみを検討すれば、全てのアルゴリズムに現れるのは「値」という考え方だけです。そして「値」(そしてその種類、プリミティブ値であろうとオブジェクトであろうと)を渡す部分の実装は舞台裏に隠されています。この観点から、 ECMAScript について抽象的に議論するなら、正確かつ厳密な意味で、「値」のみが存在し、したがって値渡しのみであるとも言えるのです。

しかし誤解を避けるために(なぜ外部の対象のプロパティが関数内部で変更されるのか?)、実装レベルに降りて検討し、共有渡しあるいは「デリファレンスや対象を完全に変更することのできない、しかし対象のプロパティを変更することのできる、ポインタ渡し」といったように議論する必要があったわけです。

各用語について

それでは、 ECMAScript におけるこの問題に関して、正確な用語の定義を与えましょう。

引き渡される値がアドレスの複製であり、値渡しの特別なケースであると明確に表現できるので、「値渡し」であり得ます。この見地からは例外オブジェクトを除く ECMAScript 上の全てが値渡しであると言えますし、 ECMAScript 仕様の抽象レベルにおいては実際にこの定義で満たされます(訳注:それでは例外オブジェクトが「何渡し」にあたるのかは、訳者は理解できておりません。どなたかおわかりの方、お教えいただけると嬉しいです)。

あるいは、この場合の戦略について特別に、「共有渡し」であるとも言えます。これは正確に伝統的な値渡し参照渡しの区別を明確にします。この場合は、引き渡される値の種類を二つに分類し、プリミティブ値は値渡し、オブジェクトは共有渡しであると表現できます。

「オブジェクトは参照渡しによって関数に参渡される」という表現は、 ECMAScript においては正式には誤りなのです

結論

この章が、評価戦略の詳細、そして ECMAScript に関係する特徴を、包括的に理解する手助けになれば幸いです。いつものように、コメントでみなさまの質問にお答えします(訳注:これでシリーズ一旦完結となります。次の ES5 シリーズに進む前に、個人ブログに全て転載をいたしますので、質問等はそちらにお寄せいただければ分かる範囲でお答えしたいと思います。転載が遅くなって申し訳ありません)。

参考文献

英語版翻訳: Dmitry A. Soshnikov[英語版].
英語版公開日時: 2010-04-10

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
協力: Zeroglif
オリジナルロシア語版公開日時: 2009-08-11

本シリーズはすべて英語版からの訳出です。

詳細 ECMA-262-3 第7章2節 OOP: ECMAScript での実装

こんにちは。大形尚弘です。

さて、ついに OOP の話も ECMAScript の実装に移ります。乗りに乗った Dmitry 先生が、前節を越えるボリュームを持って、日本全国30人(はてなブックマークの平均による導出)の ECMAScript ファンの皆さんに襲いかかります。

正直に言って、私はもう10年以上 JScript 、 JavaScript 、 ActionScript に関わってきましたが、プロトタイプの話は、内部 Prototype プロパティが「どういうわけか」コンストラクタ関数の prototype プロパティと同じ呼び名を与えられていることを発端として、常に混乱を伴ってきました。一度理解したつもりでも、すぐに忘れてしまい、わけが分からなくなってしまうのです。

それはどういうことかというと、一度もしっかり理解していなかったからです。

この節を読めば、それが理解できます。おそらく、もう二度と忘れないと思います。

しかし、ここをご覧のみなさまにもその理解が訪れるかどうかは、ひとえに、この長大な節を読み通すことができるかということにかかっています。

どうかごゆっくり、落ち着いた、集中できる、邪魔の入らない環境で、じっくりご覧下さいますようお願い申し上げます。

第7章2節 OOP: ECMAScript での実装

目次

  1. はじめに
  2. ECMAScript の OOP 実装
    1. データ型
      1. プリミティブ値型
      2. オブジェクト型
        1. 動的な特質
        2. ビルトイン、ネイティブ、ホストオブジェクト
        3. Boolean 、 String 、 Number オブジェクト
        4. リテラル記法
          1. 正規表現リテラルと RegExp オブジェクト
        5. 連想配列?
        6. 型変換
        7. プロパティ属性
        8. 内部プロパティと内部メソッド
    2. コンストラクタ
      1. オブジェクト生成のアルゴリズム
    3. プロトタイプ
      1. constructor プロパティ
      2. 明示的な prototype と暗黙の [[Prototype]] プロパティ
      3. 非標準の __proto__ プロパティ
      4. オブジェクトはコンストラクタから独立している
      5. insanceof 演算子の特徴
      6. メソッドと共有プロパティのためのストレージとしてのプロトタイプ
    4. プロパティの読み書き
      1. [[Get]] メソッド
      2. [[Put]] メソッド
      3. プロパティアクセサ
    5. 継承
      1. プロトタイプチェーン
  3. 結論
  4. 参考文献

はじめに

この節は、 ECMAScript におけるオブジェクト指向プログラミングに関する記事の第二部です。第一部ではオブジェクト指向の概論を、 ECMAScript に対応させながら検討しました。今節では、前節で定義した用語を積極的に使っていきますので、こちらをお読みになる前に、必要であれば第一部をお読みになることをお勧めします。第一部はこちら、『第7章1節 OOP: 概説』になります。

ECMAScript の OOP 実装

主要な概論を検討する長い道を通り抜け、私たちはやっと ECMAScript そのものへとたどり着きました。今や私たちは、 ECMAScript の OOP アプローチについて理解しています。再度正確な定義を与えましょう。

ECMAScript は、プロトタイプを元にした委譲による継承をサポートする、オブジェクト指向プログラミング言語です。

これからはまず、データ型の検討から分析を始めます。最初に、 ECMAScript がプリミティブ値オブジェクトを区別しているという事実に気をつける必要があります。つまり、「 JavaScript 上の全ての要素はオブジェクトです」という、いくつかの記事で掲げられるフレーズは誤りである(完全ではない)ことがわかります。プリミティブ値とは、これから詳細を検討していくというものの、データとしての状態に関するものです。

データ型

ECMAScript は動的で、ダックタイピングによる弱い型付けの、自動型変換のある言語ではありますが、それでもやはり、いくつかのデータ型を持ちます。つまり、ある瞬間において、オブジェクトは一つの具体的な型に属するわけです。

標準では9つの型が定義されており、うち6つは ECMAScript プログラムの中で直接参照することができます。

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Object

この他の3つの型は、実装内部のレベルで参照できるもので( ECMAScript 上のオブジェクトでこのような型を持つものはありません)、いつくかの演算の振る舞いを説明するために仕様によって用いられたり、中間計算値の値を保存するなどといった用途に使われます。これらは以下の3つです。

  • Reference
  • List
  • Completion

これらを簡単に見てみると、 Referene 型は deletetypeof といった演算子、 this 値などの説明のために用いられ、 base オブジェクトプロパティ名によって構成されます。 List 型は実引数のリスト( new 式や関数呼び出しの際の)のふるまいを表現します。同じく Completion 型は breakcontinuereturnthrow 文のふるまいを説明します。

訳注:これら3つの型は、 ECMA-263-5 では「仕様型( Specification Type )」という名前を与えられて区別されています。それぞれ Reference 仕様型、 List 仕様型、 Completion 仕様型となります。

プリミティブ値型

ECMAScript プログラムによって使われる6つの型に戻りましょう。最初の5つ、 UndefinedNullBooleanStringNumber は、プリミティブ値の型です。

プリミティブ値の例です。

var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;

これらの値は、低いレベルの実装中において直接表現されます。これらはオブジェクトではありません。プロトタイプも、コンストラクタも持ちません。

typeof 演算子は、適切に理解しないとあまり直観的では無いかも知れません。顕著な例は null 値の扱いです。 typeof 演算子に null 値が与えられると、null 値の型は Null と定められている事実にもかかわらず、結果は "object" となります。

alert(typeof null); // "object"

この理由は、 typeof 演算子は単に、標準に定められた表に照らし合わせて取り出された値を返すものだからです。その表には単にこうあります。null 値には、文字列 "object" が返されるべきである」と。

仕様はこれをはっきりとさせていませんが、 Brendan Eich ( JavaScript 発明者)は、 undefined に対し null が、主にオブジェクトが現れる位置で使われ、つまり本質的にオブジェクトに密接に関わるものであると述べています(オブジェクトへの「空の」参照を意味し、将来的な使用のために留保しています)。しかし、いくつかの草案において、この「現象」が一般的なバグであるとする文書もあります。また、このバグは Brendan Eich も参加するバグトラッキングシステムにも登録されていました。とはいえ、結果として、 typeof null はそのまま残されることが決まりました。つまり、 ECMA-262-3 標準では null の型を Null と定めているにもかかわらず、 “object” が返されます。

オブジェクト型

次に、オブジェクト型は( Object コンストラクタと混同しないように!。ここでは、抽象型である Object について話しています)、 ECMAScript のオブジェクトを表現する唯一の型です。

オブジェクトとは、順序づけの無い、キーと値のペアのコレクションです。

オブジェクトのキーの部分はプロパティと呼ばれます。プロパティはプリミティブ値または他のオブジェクトの入れ物です。プロパティがその値として関数を保管する場合は、メソッドと呼ばれます。

例です。

var x = { // オブジェクト "x" は3つのプロパティ: a, b, c を持つ
  a: 10, // プリミティブ値
  b: {z: 100}, // プロパティ z を持つオブジェクト "b"
  c: function () { // 関数(メソッド)
    alert('method x.c');
  }
};
 
alert(x.a); // 10
alert(x.b); // [object Object]
alert(x.b.z); // 100
x.c(); // 'method x.c'
動的な特質

第7章1節で触れたとおり、 ECMAScript におけるオブジェクトは完全に動的です。これは、プログラム実行中のどの時点においても、オブジェクトのプロパティの追加、変更、削除が可能だということを意味します。

例です。

var foo = {x: 10};
 
// 新しいプロパティを追加
foo.y = 20;
console.log(foo); // {x: 10, y: 20}
 
// プロパティの値を関数に変更
foo.x = function () {
  console.log('foo.x');
};
 
foo.x(); // 'foo.x'
 
// プロパティを削除
delete foo.x;
console.log(foo); // {y: 20}

中には変更できないプロパティ( read-only プロパティ)や、削除できないプロパティ( non-configurable プロパティ)があります。これらのケースについては、プロパティ属性の項にて簡単に検討します。

ES5 では、新しいプロパティで拡張できず、プロパティの変更や削除もできない static オブジェクトを標準化しています。これらは frozen オブジェクトとも呼ばれ、 Object.freeze(o) メソッドを適用することによって得られます。

var foo = {x: 10};
 
// オブジェクトを freeze する
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true
 
// 変更できない
foo.x = 100;
 
// 拡張できない
foo.y = 200;
 
// 削除できない
delete foo.x;
 
console.log(foo); // {x: 10}

また、 Object.preventExtensions(o) メソッドを使って拡張を防ぎ、 Object.defineProperty(o) メソッドによって特定の属性を操作することができます。

var foo = {x : 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  writable: false, // read-only(読み取り専用)
  configurable: false // non-configurable(再設定不可)
});
 
// 変更できない
foo.y = 200;
 
// 削除できない
delete foo.y; // false
 
// 拡張を防ぐ
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false
 
// 新しいプロパティを追加できない
foo.z = 30;
 
console.log(foo); {x: 10, y: 20}

詳しくは、この章(未訳)を参照下さい。

ビルトイン、ネイティブ、ホストオブジェクト

仕様上では、ネイティブオブジェクトと、ビルトインオブジェクト、それにホストオブジェクトが区別されていることも、お伝えしておかないとなりません。

ビルトイン及びネイティブオブジェクトは、 ECMAScript 仕様によって定義され、その実装における差異はあまり重要ではありません。ネイティブオブジェクトは、 ECMAScrpt 実装によって与えられる全てのオブジェクトがそれに当たります(そのうちのいくつかはビルトインであり、その他はプログラム実行中に生成されるもの、例えばユーザの定義するオブジェクトです)。

ビルトインオブジェクトはネイティブオブジェクトの派生型(部分集合)であり、プログラムが開始されるより前に先だって ECMASCript にビルトインされるものです(例えば、 parseIntMath オブジェクトなどです)。

ホストオブジェクトは、全てホスト環境、大抵はブラウザによって与えられ(訳注:最近では node.js 等も)、 windowalert といったものを含みます。

注意が必要なのは、ホストオブジェクトは時に ES 自身によって実装され、完全に仕様のセマンティック(意味的機序)に合致する場合があるということです。この観点から、主に理論上の側面ながら、それらは(非公式にネイティブ・ホストオブジェクトとも呼べるものです。しかしながら仕様では、「ネイティブ・ホスト」という考え方は定義されていません。

訳注:こうした分類はあくまで理論上のもので、例え同じオブジェクトでも、実装や実装のバージョンによって変わり得ます。 XMLHttpRequest オブジェクトは、初期バージョンの IE では当然ホストオブジェクトですが、 IE9 においては JavaScript エンジンによって実装されるネイティブ・ホストオブジェクトと考えることができます。

Boolean 、 String 、 Number オブジェクト

一部のプリミティブについては、仕様は特別なラッパーオブジェクトを定義しています。以下の3つです。

  • Boolean オブジェクト
  • String オブジェクト
  • Number オブジェクト

これらのオブジェクトは、それぞれ対応するビルトインのコンストラクタによって生成され、内部プロパティの一つとしてプリミティブ値を持ちます。こうしたオブジェクト表現はプリミティブ値に変換することができ、またその逆も可能です。

それぞれのプリミティブ型に対応するオブジェクト値の例です。

var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);
 
// プリミティブに変換
// 変換器: ToPrimitive
// "new" キーワード無しで、関数として適用する
c = Boolean(c);
d = String(d);
e = Number(e);
 
// オブジェクトに戻す
// 変換器: ToObject
c = Object(c);
d = Object(d);
e = Object(e);

加えて、特別なビルトインコンストラクタによって生成されるオブジェクトもあります。 Function (関数オブジェクトコンストラクタ)、 Array (配列コンストラクタ)、 RegExp (正規表現コンストラクタ)、 Math (算術モジュール)、 Date (日付のコンストラクタ)などです。これらのオブジェクトは全て Object 型の値であり、それらの区別は後に検討する内部プロパティによって行われます。

リテラル記法

3つのオブジェクト値、 object (オブジェクト)array (配列)regular expression(正規表現)には、それぞれオブジェクト初期化子配列初期化子正規表現リテラルと呼ばれる短縮記法があります。

// new Array(1, 2, 3); または
// array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
// と同等
var array = [1, 2, 3];
 
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
// と同等
var object = {a: 1, b: 2, c: 3};
 
// new RegExp("^\\d+$", "g"); と同等
var re = /^\d+$/g;

ObjectArrayRegExp といった名前結合に対し新しいオブジェクトを再代入する場合は注意が必要です。その代入の後にリテラル記法を使用した場合の機序は実装によって異なり得るからです。例えば、現行の Rhino 実装や、古い SpiderMonkey 1.7 では、対応するリテラル記法はそれぞれ新しく代入されたコンストラクタ名の new 文による値のオブジェクトを生成します。他の実装(現行の Spider/TraceMonkey )では、コンストラクタ名が新しいオブジェクトに再束縛されても、リテラル記法の機序は変更されません。

var getClass = Object.prototype.toString;
 
Object = Number;
 
var foo = new Object;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
var bar = {};
 
// Rhino 、 SpiderMonkey 1.7 では ― 0, "[object Number]"
// その他では:依然として "[object Object]", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// Array という名前も同様
Array = Number;
 
foo = new Array;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = [];
 
// Rhino 、 SpiderMonkey 1.7 では ― 0, "[object Number]"
// その他では:依然として "", "[object Object]"
alert([bar, getClass.call(bar)]);
 
// しかし RegExp に関しては、
// テストした全ての実装にて、
// リテラルの機序は変更されなかった
 
RegExp = Number;
 
foo = new RegExp;
alert([foo, getClass.call(foo)]); // 0, "[object Number]"
 
bar = /(?!)/g;
alert([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"

正規表現リテラルと RegExp オブジェクト

しかし、ここでご注意いただきたいのは、 ES3 において最後の二つ、正規表現が意味的に同じであるというケースにおいても、実は異なる点があるということです。ある一つの正規表現リテラルが、パース時に生成されるたった一つのインスタンスのみを生成しこれが再利用される一方、 RegExp コンストラクタは常に新しいオブジェクトを生成します。これは例えば正規表現オブジェクトの lastIndex プロパティを使用する際に問題となり得ます。正規表現テストが上手く動かない場合があるのです。

for (var k = 0; k < 4; k++) {
  var re = /ecma/g;
  alert(re.lastIndex); // 0, 4, 0, 4
  alert(re.test("ecmascript")); // true, false, true, false
}
 
// に対し
 
for (var k = 0; k < 4; k++) {
  var re = new RegExp("ecma", "g");
  alert(re.lastIndex); // 0, 0, 0, 0
  alert(re.test("ecmascript")); // true, true, true, true
}

ES5 ではこの問題は解決され、正規表現リテラルが常に新しいオブジェクトを生成します(訳注:訳者の手元の IE 環境では、バージョン 6 で既にこの問題は解決しているようです。ただし、ループ中で正規表現リテラルを使用する際は、念のためご自身でご確認ください)

連想配列?

しばしば、いくつかの記事や議論の中で、 JavaScript のオブジェクト(そして大抵は宣言的な形式、オブジェクト初期化子 {} で生成されたもの)は、ハッシュテーブル、または単にハッシュ( Ruby や Perl からの用語)、あるいは連想配列( PHP からの用語)、辞書( Python からの用語)などと呼ばれます。

この用語の使用法は、具体的な実装に対する習慣によるものです。実際の処これらは十分に同等と言えるものであり、「キーバリュー」ペアのストレージとして見れば、論理的に全く「連想配列」「ハッシュテーブル」に対応しています。さらに言えば、ハッシュテーブルという抽象的なデータ型は実装のレベルでも利用できますし、また実際一般的に使われてもいます。

しかしながら、こうした用語は概念的な考え方を表現するものですが、一方 ECMAScript において実際には、これは技術的に正しいものであるとは言えません。前述したとおり、 ECMAScript はたった一つの オブジェクト型とその「派生型」を持ち、「キーバリュー」ペアのストレージという観点からは、これらの間に全く違いは無いのです。したがって、そこに個別の特別な用語(ハッシュやその他)はありません。なぜならどんなオブジェクトも、その内部プロパティに関わらず、これらのペアを保管できるからです。

var a = {x: 10};
a['y'] = 20;
a.z = 30;
 
var b = new Number(1);
b.x = 10;
b.y = 20;
b['z'] = 30;
 
var c = new Function('');
c.x = 10;
c.y = 20;
c['z'] = 30;
 
// などなど ― どんなオブジェクト「派生型」であれ

さらに、 ECMAScript のオブジェクトはその委譲の性質から空になり得ませんので、「ハッシュ」という用語も不適当になり得ます。

Object.prototype.x = 10;
 
var a = {}; // 「空の」「ハッシュ」を生成
 
alert(a["x"]); // 10 、しかし空では無かった
alert(a.toString); // 関数
 
a["y"] = 20; // 「ハッシュ」に新しいペアを追加
alert(a["y"]); // 20
 
Object.prototype.y = 20; // プロトタイプにプロパティを追加
 
delete a["y"]; // 削除
alert(a["y"]); // しかしキーバリューは依然として存在する ― 20

ES5 は、プロトタイプを持たないオブジェクトを生成する方法を標準化しました(未訳)。つまり、そのプロトタイプが null にセットされるのです。これは、 Object.create(null) メソッドを用いて行われます。この観点からは、こうしたオブジェクトは単なるハッシュテーブルであると言えます。

var aHashTable = Object.create(null);
console.log(aHashTable.toString); // undefined

そして、いくつかのプロパティは特別な getter/setter を持ち得ますので、さらに混乱の元になるかもしれません。

var a = new String("foo");
a['length'] = 10;
alert(a['length']); // 3

とは言え、「ハッシュ」がもし「プロトタイプ」を持つことができるとしても(例えば Ruby や Python では、クラスがハッシュオブジェクトから委譲できます)、 ECMAScript では、やはりこの用語は不適当になり得ます。なぜなら、プロパティアクセサの種類ドットまたは括弧記法)の間に意味的な違いは無いからです。

さらに、 ECMAScript では、「プロパティ」の考え方が、「キー」「配列インデックス」「メソッド」「プロパティ」に意味的には区別されていません。これらは全てプロパティであり、プロトタイプチェーンの探索に関する読み書きのアルゴリズムの一般的な法則に従います。

以下の Ruby の例では意味論的な違い、そして結果として用語がそれぞれ異なり得ることを見て取ることができます。

a = {}
a.class # ハッシュ
 
a.length # 0
 
# 新しい「キーバリュー」ペア
a['length'] = 10;
 
# しかしドット記法の意味は依然として異なり、
# 「キー」ではなく、
# 「プロパティ・メソッド」へのアクセスを意味する
 
a.length # 1
 
# そして括弧記法は、
# ハッシュの「キー」へのアクセスを提供する
 
a['length'] # 10
 
# 動的に、 Hash クラスを新しい
# プロパティ・メソッドで拡張し、
# 委譲によって既に生成されたオブジェクトにも
# 利用可能になる
 
class Hash
  def z
    100
  end
end
 
# 新しい「プロパティ」が利用可能
 
a.z # 100
 
# しかしこれは「キー」ではない
 
a['z'] # nil

ECMA-262-3 仕様は、「ハッシュ」(や同様のもの)の考え方を定義していません。しかし、(実装面・技術的ではない)論理的なデータ構造について言うのであれば、オブジェクトをそのように呼ぶことは可能です。

型変換

オブジェクトをプリミティブ値に変換するには、 valueOf メソッドが利用できます。既に触れたように、(ある型の)コンストラクタを関数として、 new 演算子無しで呼び出しても、オブジェクト型からプリミティブ値への変換を行えます。この変換には全くもって、暗黙的な valueOf メソッドの呼び出しが用いられています。

var a = new Number(1);
var primitiveA = Number(a); // 暗黙の "valueOf" 呼び出し
var alsoPrimitiveA = a.valueOf(); // 明示的な呼び出し
 
alert([
  typeof a, // "object"
  typeof primitiveA, // "number"
  typeof alsoPrimitiveA // "number"
]);

このメソッドによって、オブジェクトは(そのままの形で)さまざまな演算に参加することができます。加算の例を見てみましょう。

var a = new Number(1);
var b = new Number(2);
 
alert(a + b); // 3
 
// あるいはさらに
 
var c = {
  x: 10,
  y: 20,
  valueOf: function () {
    return this.x + this.y;
  }
};
 
var d = {
  x: 30,
  y: 40,
  // オブジェクト "c" が持つ
  // .valueOf の機能を拝借
  valueOf: c.valueOf
};
 
alert(c + d); // 100

valueOf メソッドの値は、デフォルトでは(オーバーライドされていなければ)オブジェクトの型によってさまざまです。 this 値を返すものもあれば(例えば Object.prototype.valueOf() )、計算された値( Date.prototype.valueOf() のように)ある日の UNIX 時間を返すものもあります。

var a = {};
alert(a.valueOf() === a); // true, "valueOf" が this 値を返した
 
var d = new Date();
alert(d.valueOf()); // 時間
alert(d.valueOf() === d.getTime()); // true

オブジェクトのプリミティブな表象にはもう一つあります。文字列としての現れ方です。これは toString メソッドが担当します。このメソッドに関しても、いくつかの演算にて自動的に適用されます。

var a = {
  valueOf: function () {
    return 100;
  },
  toString: function () {
    return '__test';
  }
};
 
// この演算では、
// toString メソッドが
// 自動で呼び出される
alert(a); // "__test"
 
// ここでは .valueOf() メソッドが呼ばれる
alert(a + 10); // 110
 
// しかし同じ演算でも
// valueOf メソッドが無ければ、
// toString メソッドで代替される
delete a.valueOf;
alert(a + 10); // "_test10"

Object.prototype に定義された toString メソッドには特別な意味があります。後に検討する、内部プロパティ [[Class]] の値を返すのです。

ToPrimitive 変換と同じく、逆に値をオブジェクト型に変換する ToObject 変換もあります。

明示的に ToObject を呼び出す一つの方法は、ビルトインの Object コンストラクタを関数として(いくつかの型に関しては new 演算子付きも可能)呼び出すことです。

var n = Object(1); // [object Number]
var s = Object('test'); // [object String]
 
// いくつかの型に関しては
// new 演算子付きで Object を呼び出しても
// ToObject 変換が可能
var b = new Object(true); // [object Boolean]
 
// ただし引数無しで呼び出すと
// 単なるオブジェクトを生成する
var o = new Object(); // [object Object]
 
// もし Object 関数への引数が
// 既にオブジェクト値だった場合、
// 単にそれをそのまま戻す
var a = [];
alert(a === new Object(a)); // true
alert(a === Object(a)); // true

ビルトインコンストラクタを呼び出す際の new 演算子の有無に関しては一般的なルールは無く、コンストラクタ次第です。例えば ArrayFunction コンストラクタは、コンストラクタとして( new 付きで)呼び出した際も、単なる関数として( new 無しで)呼び出した際も、同じ結果を出力します。

var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]
 
var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]

演算子によっては、適用される際に明示的、あるいは暗黙的な型キャストが行われます。

var a = 1;
var b = 2;
 
// 暗黙的
var c = a + b; // 3 、 number
var d = a + b + '5' // "35" 、 string
 
// 明示的
var e = '10'; // "10" 、 string
var f = +e; // 10 、 number
var g = parseInt(e, 10); // 10 、 number
 
// 等々
プロパティ属性

全てのプロパティはいくつかの属性を持ちます(訳注:下記は ES3 でのものです)。

  • {ReadOnly} ― プロパティへの値の書き込もうとする試みは無視される。しかし、 ReadOnly プロパティはホスト環境の作用によって変化し得るため、 ReadOnly が「定数」を意味するわけではない。
  • {DontEnum} ― プロパティが for.. in ループに列挙されない。
  • {DontDelete} ― プロパティへの delete 演算子の作用は無視される。
  • {Internal} ― プロパティは内部的なものである。名前を持たず、実装レベルでのみ使用される。こうしたプロパティは、 ECMAScript のプログラムからはアクセスできない。

ES5 では、 {ReadOnly}{DontEnum}{DontDelete} はそれぞれ [[Writable]][[Enumerable]][[Configurable]]名称変更されました(未訳)。また、これらの属性は Object.defineProperty などのメソッドを使って手動で修正することが可能です。

var foo = {};
 
Object.defineProperty(foo, "x", {
  value: 10,
  writable: true, // {ReadOnly} = false
  enumerable: false, // {DontEnum} = true
  configurable: true // {DontDelete} = false 
});
 
console.log(foo.x); // 10
 
// 属性の集合は記述子と呼ばれます
var desc = Object.getOwnPropertyDescriptor(foo, "x");
 
console.log(desc.enumerable); // false
console.log(desc.writable); // true
// etc.
内部プロパティと内部メソッド

オブジェクトもまた、実装の一部であり ECMAScript プログラムからは直接にアクセスすることのできない(とはいえ今後見ていくように実装によってはアクセス可能な)、いくつかの内部プロパティを持ちます。これらのプロパティは、慣習として二重の角括弧 [[ ]] で囲われます。

ここではこれらのいくつかに触れていきますが(全てのオブジェクトに必須なもの)、その他のプロパティの詳細は仕様に見ることができます。

全てのオブジェクトは、以下の内部プロパティと内部メソッドを実装しなければなりません。

  • [[Prototype]] ― このオブジェクトのプロトタイプ(詳しくは後に検討します)。
  • [[Class]] ― オブジェクトの種類を文字列で表すもの(例えば ObjectArrayFunction など)。オブジェクトを区別するために使われる。
  • [[Get]] ― プロパティの値を取得するためのメソッド。
  • [[Put]] ― プロパティの値を設定するためのメソッド。
  • [[CanPut]] ― プロパティへの書き込みが可能かチェックする。
  • [[HasProperty]] ― オブジェクトが指定のプロパティを持つかチェックする。
  • [[Delete]] ― オブジェクトからプロパティを削除する。
  • [[DefaultValue]] ― オブジェクトに対応するプリミティブ値を返す(この値の取得には valueOf メソッドが呼ばれる。いくつかのオブジェクトに関しては、 TypeError 例外が投げられる)。

ECMAScript プログラムからの [[Class]] プロパティの取得は、 Object.prototype.toString() を経由して間接的に可能です。このメソッドは以下の文字列 "[object " + [[Class]] + "]" を返します。例えば、

var getClass = Object.prototype.toString;
 
getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// など

この機能はしばしばオブジェクトの種類を確認するために用いられますが、仕様によればホストオブジェクトの内部 [[Class]] プロパティはビルトインオブジェクトの [[Class]] プロパティを含め何でも良いと規定されていることに注意が必要です。つまり理論的には100%保証するチェックはできないということです。例えば、 document.childNodes.item(...) メソッドの IE における [[Class]] プロパティの値は "String" です(他の実装では、 "Function" が返されます)。

// IE では "String" 、 その他では "Function"
alert(getClass.call(document.childNodes.item));

訳注: ES5 仕様において、ホストオブジェクトの Class プロパティが、 "Arguments""Array""Boolean""Date""Error""Function""JSON""Math""Number""Object""RegExp""String" 以外の値を取ると規定されました。

コンストラクタ

既に触れたように、 ECMAScript におけるオブジェクトは、いわゆるコンストラクタを経由して生成されます。

コンストラクタは、新しく生成されるオブジェクトを生成し、初期化する関数です。

生成(メモリアロケーション)に関しては、コンストラクタ関数の [[Constructor]] 内部メソッドが担当します。新しいオブジェクトのメモリを割り当てるために、この内部メソッドのふるまいが規定され、全てのコンストラクタ関数がこのメソッドを利用します。

そして初期化に関しては、新しく生成されたオブジェクトのコンテキストで関数を呼び出すことで行われます。ここではコンストラクタ関数の [[Call]] 内部メソッドが担当します。

注意としては、ユーザコードからアクセス可能なのはこのうち初期化フェーズのみだということです。しかし初期化フェーズにおいても、最初に生成された this オブジェクトを無視して、異なるオブジェクトを戻すことが可能です。

function A() {
  // 新しく生成されたオブジェクトを更新
  this.x = 10;
  // しかし異なるオブジェクトを戻す
  return [1, 2, 3];
}
 
var a = new A();
console.log(a.x, a); undefined, [1, 2, 3]

『第5章 関数』にて検討したように、関数オブジェクトの生成アルゴリズムを見ると、関数とはプロパティの中でもこの内部 [[Construct]] 及び [[Call]] プロパティを持ち、明示的な prototype プロパティ(将来この関数をコンストラクタとして生成されるオブジェクトのプロトタイプへの参照)を持つネイティブオブジェクトであることが分かります(下記の例での NativeObject とは、 ECMA-262-3 の「ネイティブオブジェクト」の考え方のために私が慣習的に名付けた擬似コード上の名称であって、ビルトインコンストラクタではありません)。

F = new NativeObject();
 
F.[[Class]] = "Function"
 
.... // その他のプロパティ
 
F.[[Call]] = <reference to function> // 関数自身
 
F.[[Construct]] = internalConstructor // 一般的な内部コンストラクタ
 
.... // その他のプロパティ
 
// F コンストラクタで生成されるオブジェクトのプロトタイプ
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype

したがって、 [[Class]] プロパティ( "Function" となる)に加えて [[Call]] が、オブジェクトを区別する点で重要になります。つまり、内部 [[Call]] プロパティを持つオブジェクトが、関数と呼ばれるのです。こうしたオブジェクトに対して、 typeof 演算子は "function" という値を返します。ただしこれは主にネイティブオブジェクトの話であって、呼び出し可能なホストオブジェクトの場合は、実装によっては typeof 演算子が( [[Class]] プロパティに負けず劣らず)他の値を返します。例えば、 IE において window.alert(...) は...、

// IE では ― "Object", "object" 、その他では "Function", "function"
alert(Object.prototype.toString.call(window.alert));
alert(typeof window.alert); // "Object"

内部 [[Construct]] メソッドはコンストラクタ関数に new 演算子を付与することでアクティベートされます。申し上げたように、このメソッドはメモリアロケーションとオブジェクトの生成を担当しています。もし実引数が無い場合は、コンストラクタ関数の呼び出し括弧は省略可能です。

function A(x) { // コンストラクタ A
  this.x = x || 10;
}
 
// 実引数が無い場合、
// 呼び出し括弧は省略可能
var a = new A; // or new A();
alert(a.x); // 10
 
// 明示的な x 引数の
// 引き渡し
var b = new A(20);
alert(b.x); // 20

既にご存じの通り、コンストラクタ中の(初期化フェーズにおける) this 値新しく生成されたオブジェクトにセットされます。

それでは次に、オブジェクト生成のアルゴリズムについて検討してみましょう。

オブジェクト生成のアルゴリズム

内部 [[Construct]] メソッドのふるまいは以下のように記述できます。

F.[[Construct]](initialParameters):
 
O = new NativeObject();
 
// Class プロパティが "Object" にセットされる、つまり単純なオブジェクト
O.[[Class]] = "Object"
 
// この瞬間において F.protottype を参照している
// オブジェクトを取得
var __objectPrototype = F.prototype;
 
 // もし __objectPrototype がオブジェクトならば
O.[[Prototype]] = __objectPrototype
// そうでは無い場合、
O.[[Prototype]] = Object.prototype;
// ここで O.Prototype とは生成されるオブジェクトのプロトタイプ
 
// F.Call を呼び出して
// 新しく生成されたオブジェクトを初期化
// this 値としては新しく生成されたオブジェクト ― O
// Call への実引数は F への initialParameters と同じ
R = F.[[Call]](initialParameters); this === O;
// R は Call の戻り値
// JS 的に見ればこのようになる
// R = F.apply(O, initialParameters);
 
// R がオブジェクトなら
return R
// そうでなければ
return O

二つの重要な特徴に注目して下さい。

最初に、生成されたオブジェクトのプロトタイプは、その瞬間のコンストラクタ関数の prototype プロパティから取られます(これの意味することは、ある一つのコンストラクタから生成された二つのオブジェクトのプロトタイプは異なる可能性がある、ということです。なぜならコンストラクタ関数の prototype プロパティは、いつでも変更し得るからです)。

次に、上で触れたように、オブジェクト初期化時に [[Call]]オブジェクトを戻した場合、まさにそのオブジェクトが new 式全体の結果として用いられます。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10 - 委譲によって、プロトタイプから
 
// 関数の .prototype プロパティを
// 新しいオブジェクトにセットする
// .constructor プロパティを
// 明示的に定義している理由は後に説明する
A.prototype = {
  constructor: A,
  y: 100
};
 
var b = new A();
// オブジェクト "b" が新しいプロトタイプを持つ
alert(b.x); // undefined
alert(b.y); // 100 - 委譲によって、プロトタイプから
 
// しかし、オブジェクト "a" のプロトタイプは
// 依然として古いままである(理由は後述する)
alert(a.x); // 10 - 委譲によって、プロトタイプから
 
function B() {
  this.x = 10;
  return new Array();
}
 
// もし "B" コンストラクタが何も戻さなければ、
// (あるいは this を戻したとしたら)
// this オブジェクトが使われるが、
// この場合では配列である
var b = new B();
alert(b.x); // undefined
alert(Object.prototype.toString.call(b)); // [object Array]

それでは、プロトタイプについてより詳しく見ていきましょう。

プロトタイプ

全てのオブジェクトはプロトタイプを持ちます(システムオブジェクトには例外もあります)。プロトタイプとの通信は、内部的で、暗黙的で、直接にはアクセス不可の [[Prototype]] プロパティを経由して行われます。プロトタイプは、オブジェクトまたは null 値を取ります。

constructor プロパティ

上の例には、二つの重要なポイントがありました。一つ目は関数の prototype プロパティの constructor プロパティに関係しています。

関数オブジェクト生成のアルゴリズムで見たとおり、 constructor プロパティは、関数の生成時に、関数の prototype プロパティ(訳注:この関数によって生成されたオブジェクトにとってのプロトタイプ)にセットされます。このプロパティの値は、関数自身そのものへの循環参照になっています。

function A() {}
var a = new A();
alert(a.constructor); // function A() {} 、委譲によって
alert(a.constructor === A); // true

しばしば、このケースについて誤解があります。 constructor プロパティを、誤って生成されたオブジェクト自身のプロパティと扱ってしまうのです。しかし、見てきたようにこのプロパティはプロトタイプに属し、継承を通じてオブジェクトにアクセスできるのです。

継承された constructor プロパティを使って、インスタンスは間接的にプロトタイプオブジェクトへの参照を得ることができるのです。

function A() {}
A.prototype.x = new Number(10);
 
var a = new A();
alert(a.constructor.prototype); // [object Object]
 
alert(a.x); // 10 、委譲を通じて
// the same as a.Prototype.x
alert(a.constructor.prototype.x); // 10
 
alert(a.constructor.prototype.x === a.x); // true

しかしご注意いただきたいのは、関数の constructor 及び prototype プロパティ共に、オブジェクトが生成された後で再定義されることが可能だということです。この場合、オブジェクトは上記のメカニズムへの参照を失うことになります。

もしここで、関数の prototype プロパティを通じてオリジナルのプロトタイプに新しいプロパティを追加、または既存のプロパティを変更すると、インスタンスからはその新しく追加・変更されたプロパティを見ることになります。

しかし、もし関数の prototype プロパティを完全に変更してしまったら(新しいオブジェクトを代入することで)、オリジナルのコンストラクタ(もちろんオリジナルのプロトタイプも)への参照は失われてしまいます。もちろんこれは、新しく生成したオブジェクトが constructor プロパティを(自身に)持たないためです。

function A() {}
A.prototype = {
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // false!

従って、この参照を戻すためには、手動で回復してあげなければなりません。

function A() {}
A.prototype = {
  constructor: A,
  x: 10
};
 
var a = new A();
alert(a.x); // 10
alert(a.constructor === A); // true

手動で回復された constructor プロパティは、失われたオリジナルに比較して、 {{DontEnum}} 属性を持たないことには注意が必要です。そしてこの結果として、 A.prototype を通じて for.. in ループ中に現れてしまいます。

ES5 では、 {{Enumerable}} 属性を通じて、プロパティの列挙可能状態を操作することができるように(未訳)なりました。

var foo = {x: 10};
 
Object.defineProperty(foo, "y", {
  value: 20,
  enumerable: false // {DontEnum} = true だったもの
});
 
console.log(foo.x, foo.y); // 10 、 20
 
for (var k in foo) {
  console.log(k); // "x" のみ
}
 
var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");
 
console.log(
  xDesc.enumerable, // true
  yDesc.enumerable  // false
);
明示的な prototype と暗黙の [[Prototype]] プロパティ

時折、誤ってオブジェクトのプロトタイプを関数の prototype プロパティへの明示的な参照であると混同してしまうことがあります。これは完全な誤りとは言えませんが、正確ではありません。実際は、これらは共に、全く同じオブジェクト、つまりオブジェクトの [[Prototype]] プロパティへの参照なのです(訳注:どちらかが実体で、どちらかが参照、という関係では無く、どちらも参照でしかありません)。

a.[[Prototype]] ----> Prototype <---- A.prototype

さらにインスタンスの [[Prototype]] は、その値を(その参照を)、まさしくコンストラクタの prototype プロパティから、オブジェクト生成時に、得るのです。

しかしながら、コンストラクタの prototype プロパティを置き換えても、既に生成されたオブジェクト のプロトタイプには影響しません。変更されるのは、コンストラクタの prototype プロパティのみなのです!。つまり、今後新しく生成されるオブジェクトは、新しいプロトタイプを持ち、( prototype プロパティが変更される前に)既に生成されたオブジェクトは、古いプロトタイプへの参照を持ち続け、この参照は以後変更できないのです

// A.prototype が変更される前
a.[[Prototype]] ----> Prototype <---- A.prototype
 
// 変更された後
A.prototype ----> New prototype // 新しいオブジェクトはこのプロトタイプを持つことになる
a.[[Prototype]] ----> Prototype // 古いプロトタイプへの参照を持ち続ける

例です。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
A.prototype = {
  constructor: A,
  x: 20
  y: 30
};
 
// オブジェクト "a" は
// 暗黙の Prototype 参照を通じ
// 古いプロトタイプに委譲を行う
alert(a.x); // 10
alert(a.y) // undefined
 
var b = new A();
 
// しかし新しいオブジェクトは
// 生成時に新しいプロトタイプへの参照を取得する
alert(b.x); // 20
alert(b.y) // 30

訳注:分かりにくいポイントなので補足しますと、コンストラクタ関数の prototype プロパティそのものの参照先を変更した場合は、上記の通りです。対して、コンストラクタ関数の prototype プロパティであるオブジェクトのプロパティを一部変更した場合は、その変更が既に生成されたオブジェクトに継承を通じて影響します。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
// prototype 「の」プロパティを追加
A.prototype.y = 20;
 
// インスタンスにも継承を通じて反映
alert(a.y); // 20
 
// prototype 「そのもの」を変更
A.prototype = {
  constructor: A,
  x: 20,
  y: 30
};
 
// インスタンスから参照されているプロトタイプは
// 古いまま
alert(a.x); // 10
alert(a.y); // 20

従って、 JavaScript に関数記事などでしばしば見受けられる、「プロトタイプそのものの動的な変更は全てのオブジェクトに影響し、それらのオブジェクトは皆新しいプロトタイプを持つことになる」という主張は、誤りです(訳注:補足したとおり「プロトタイプのプロパティへの変更」であれば、全てのオブジェクトに影響します)。新しいプロトタイプは、新しいオブジェクトにのみ影響します。

重要はルールはこうです。オブジェクトのプロトタイプは、オブジェクトの生成時にセットされ、その後は新しいオブジェクトに変更できません。コンストラクタからの明示的な prototype参照を使えば、それが(オブジェクトのプロトタイプとコンストラクタの prototype プロパティが共に)同じオブジェクトを指している限り、オブジェクトのプロトタイプに新しいプロパティを追加したり、既存のプロパティを変更することだけは可能です。

非標準の __proto__ プロパティ

ところが、一部の実装、例えば SpiderMonkey は、(暗黙的であってオブジェクト生成後にはユーザコードからは変更できないはずの)オブジェクトのプロトタイプへの明示的な参照を持っています。非標準の __proto__ プロパティです。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
var __newPrototype = {
  constructor: A,
  x: 20,
  y: 30
};
 
// 新しいオブジェクトへの参照
A.prototype = __newPrototype;
 
var b = new A();
alert(b.x); // 20
alert(b.y); // 30
 
// オブジェクト "a" は依然として
// 古いプロトタイプを持つ
alert(a.x); // 10
alert(a.y); // undefined
 
// プロトタイプを明示的に変更
a.__proto__ = __newPrototype;
 
// オブジェクト "a" も
// 新しいオブジェクトを参照するようになる
alert(a.x); // 20
alert(a.y); // 30

ES5 では、 Object.getPrototypeOf(O) メソッドが導入されました。これは直接オブジェクトの [[Prototype]] プロパティ、つまりインスタンスのオリジナルのプロトタイプを返すものです。しかし __proto__ と異なるのは、これは getter であり、プロトタイプをセットすることはできないということです。

var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true
オブジェクトはコンストラクタから独立している

インスタンスのプロトタイプはコンストラクタ及びコンストラクタの prototype プロパティから独立していますから、その主な用途、オブジェクトの生成が終了した後は、コンストラクタは削除できます。コンストラクタが削除されてもプロトタイプオブジェクトは [[Prototype]] プロパティを通じて参照され、存在し続けます(訳注:コンストラクタの prototype プロパティもまた、プロトタイプオブジェクトへのある一つの参照でしかないということ。この「参照」がオブジェクト(インスタンス)生成時にオブジェクトに引き渡されるということを再度思い出して下さい。より根本的には、 JavaScript においてオブジェクトを格納する変数・プロパティはすべて参照である、ということも意識しておくと理解しやすいと思います)。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
// 明示的なコンストラクタへの参照である
// "A" を null にセットする
// (訳注:コンストラクタ関数へのある一つの参照を削除しただけ)
A = null;
 
// しかし依然として、
// .constructor プロパティが変更されていなければ
// 他のオブジェトからの間接的な参照を通じて
// オブジェクトを生成することができる
var b = new a.constructor();
alert(b.x); // 10
 
// 両方の暗黙的な参照を削除する
delete a.constructor.prototype.constructor;
delete b.constructor.prototype.constructor;
 
// これで "A" コンストラクタのオブジェクトを
// 生成することはできなくなったが、
// 生成された二つのオブジェクトには、
// それぞれのプロトタイプへの参照が存在し続ける
alert(a.x); // 10
alert(b.x); // 10
insanceof 演算子の特徴

instanceof 演算子の働きには、コンストラクタの prototype プロパティを経由した、プロトタイプへの明示的な参照が関わっています。

この演算子は、コンストラクタに対してではなく、まさにプロトタイプチェーンに対して働きます。これを鑑みるに、いくつか誤解があるように思われます。例えば、このような検査があったとします。

if (foo instanceof Foo) {
  ...
}

これは、オブジェクト foo が、コンストラクタ Foo によって生成されたかどうかの検査では無いのです!。

instanceof 演算子が行っているのは、オブジェクトのプロトタイプ( foo.[[Prototype]] )を取り出し、プロトタイプチェーン( foo.[[Prototype]]foo.[[Prototype]].[[Prototype]]foo.[[Prototype]].[[Prototype]] ...)中に Foo.prototype存在を検査することだけです。

これをサンプルコードで見てみましょう。

function A() {}
A.prototype.x = 10;
 
var a = new A();
alert(a.x); // 10
 
alert(a instanceof A); // true
 
// もし A.prototype を
// null にセットしたら...
A.prototype = null;
 
// ... "a" オブジェクトは
// 依然としてそのプロトタイプに
// a.Prototype を経由して
// アクセスできる
alert(a.x); // 10
 
// しかし、 instanceof 演算子は
// 働かなくなる
// なぜならその検査は
// コンストラクタのプロトタイププロパティに対して
// 行われるからである
alert(a instanceof A); // エラー、 A.prototype はオブジェクトではない

これに対し、あるコンストラクタからオブジェクトを生成しつつ、 instanceof異なるコンストラクタとの検査に true を返すようにすることが可能です。オブジェクトの [[Prototype]] プロパティ及びコンストラクタの prototype プロパティを、同じオブジェクトに向けさえすればいいのです。

function B() {}
var b = new B();
 
alert(b instanceof B); // true
 
function C() {}
 
var __proto = {
  constructor: C
};
 
C.prototype = __proto;
b.__proto__ = __proto;
 
alert(b instanceof C); // true
alert(b instanceof B); // false
メソッドと共有プロパティのためのストレージとしてのプロトタイプ

ECMAScript におけるプロトタイプの最も有用な応用は、オブジェクトのメソッドデフォルト状態共有プロパティのストレージとしての役割です。

実際、オブジェクトはそれぞれ独自の状態を持つことができます。しかし、メソッドは大抵同じです。したがって、メモリ使用量の最適化のために、メソッドは通例プロトタイプ中に定義されます。これは、このコンストラクタから生成された全てのインスタンスが、常に同じメソッドを共有するということを意味します。

function A(x) {
  this.x = x || 100;
}
 
A.prototype = (function () {
 
  // コンテキストを初期化、
  // 補助的なオブジェクトを使う
 
  var _someSharedVar = 500;
 
  function _someHelper() {
    alert('internal helper: ' + _someSharedVar);
  }
 
  function method1() {
    alert('method1: ' + this.x);
  }
 
  function method2() {
    alert('method2: ' + this.x);
    _someHelper();
  }
 
  // これがプロトタイプそのもの
  return {
    constructor: A,
    method1: method1,
    method2: method2
  };
 
})();
 
var a = new A(10);
var b = new A(20);
 
a.method1(); // method1: 10
a.method2(); // method2: 10 、 internal helper: 500
 
b.method1(); // method1: 20
b.method2(); // method2: 20 、 internal helper: 500
 
// どちらのオブジェクトも
// 同じプロトタイプの
// 同じメソッドを使っている
alert(a.method1 === b.method1); // true
alert(a.method2 === b.method2); // true

プロパティの読み書き

前述したように、プロパティの読み書きはそれぞれ内部メソッド [[Get]][[Put]] によって行われます。これらのメソッドは、プロパティアクセサ、つまりドット記法及び括弧記法によってアクティベートされます。

// 書き込み
foo.bar = 10; // Put が呼ばれる
 
// 読み出し
console.log(foo.bar); // 10 、Get が呼ばれる
console.log(foo['bar']); // 上に同じ

[[Get]] メソッドはオブジェクトのプロトタイプチェーン中のプロパティも考慮します。したがって、プロトタイプのプロパティはオブジェクト自身のプロパティ同様にアクセスが可能です。

対して [[Put]] メソッドは、オブジェクト自身のプロパティを作成または更新するもので、プロトタイプ中の同名のプロパティを隠すように働きます。

これらのメソッドの働きを、擬似コードで見ていくことにしましょう。

[[Get]] メソッド

[[Get]]

O.[[Get]](P):
 
// 自身のプロパティがあれば
// それを返す
if (O.hasOwnProperty(P)) {
  return O.P;
}
 
// そうでなければ、プロトタイプを分析する
var __proto = O.[[Prototype]];
 
// もしプロトタイプが無ければ
// (例えばチェーンの最後の要素、
// Object.prototype.Prototype
// は null である)
// undefined を返す
if (__proto === null) {
  return undefined;
}
 
// プロトタイプがあれば、
// プロトタイプに対し再帰的に Get メソッドを呼び出す
// つまりプロトタイプチェーン中の
// ある要素(プロトタイプ)に対し
// プロパティを見つけようとし、
// 見つからなければ
// Prototype が null に等しくなるまで
// そのプロトタイプのプロトタイプへと続く
return __proto.[[Get]](P)

補足としては、 [[Get]] メソッドはケースによっては undefined を返しますので、以下のように、変数の存在をチェックすることが可能です。

if (window.someObject) {
  ...
}

ここで、プロパティ someObjectwindow 、あるいはそのプロトタイプ、さらにそのプロトタイプのプロトタイプ...( null になるまで)に見つからなければ、アルゴリズムに従い、 undefined が返されます。

正確な存在の確認には、 in 演算子が使われることも覚えておいてください。こちらもプロトタイプチェーンを考慮します。

if ('someObject' in window) {
  ...
}

in 演算子は、例えば someObject が存在しながら値が false である場合でも、プロパティの存在を確認できます。もう一つ前の例では、 if を通過できません。

[[Put]] メソッド

[[Put]]

O.[[Put]](P, V):
 
// このプロパティに
// 書き込めない場合は
// 終了する
if (!O.[[CanPut]](P)) {
  return;
}
 
// もしオブジェクトが
// それ自身にプロパティを持っていなければ
// それを生成する
// 全ての属性は空に設定される( false になる)(訳注: ES3 )
if (!O.hasOwnProperty(P)) {
  createNewProperty(O, P, attributes: {
    ReadOnly: false,
    DontEnum: false,
    DontDelete: false,
    Internal: false
  });
}
 
// 値をセットする
// 属性は変更されない
O.P = V
 
return;
プロパティアクセサ

前述の通り、内部メソッド [[Get]][[Put]]プロパティアクセサによってアクティベートされます。これは、 ECMAScript においてはドット記法または括弧記法を用いて利用可能です。ドット記法はプロパティ名が妥当な識別子であり予めその名前を知っている場合に用いられ、括弧記法ではプロパティの名前を動的に形成することができます。

var a = {testProperty: 10};
 
alert(a.testProperty); // 10 、ドット記法
alert(a['testProperty']); // 10 、括弧記法
 
var propertyName = 'Property';
alert(a['test' + propertyName]); // 10 、動的なプロパティによる括弧記法

ここに一つ重要な特徴があります。プロパティアクセサは、常に左辺のオブジェクトに関し ToObject 変換を行います。そして、この暗黙の変換のために、大雑把に言って「 JavaScript における全ての要素はオブジェクト」と表現することが可能なのです(とはいえもちろん皆さんはご存じの通り、プリミティブ値というものがありますから、「全て」ではありません)。

プリミティブ値に対してプロパティアクセサを用いると、その値に対応する中間ラッパーオブジェクトを生成することになります。そして演算が終了した後、このラッパーは削除されます。

例です。

var a = 10; // プリミティブ値
 
// しかし、まるでオブジェクトであるように
// メソッドにアクセスできる
alert(a.toString()); // "10"
 
// さらに、 Put を呼び出して
// プリミティブ値 "a" に
// 新しいプロパティを作る(ことを試みる)
// ことさえできる
a.test = 100; // seems, it even works
 
// しかし Get
// このプロパティの値を返さない
// アルゴリズムに従い undefined が返される
alert(a.test); // undefined

ではなぜ、この例では「プリミティブ」値が toString メソッドにアクセスしながら、新しく作られた test プロパティにはアクセスできないのでしょうか?

答えはシンプルです。

まず最初に、ご説明したとおり、プロパティアクセサが適用されると、それはプリミティブではなく中間オブジェクトになります。この場合 new Number(a) が使われ、委譲によってプロトタイプチェーン中に toString メソッドが見つかります。

// a.toString() を評価するアルゴリズム
 
1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;

次に、 [[Put]] メソッドも test プロパティの評価時に、またそれ独自のラッパーオブジェクトを生成します。

// a.test = 100: を評価するアルゴリズム
 
1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;

ステップ3でラッパーが削除され、同様に新しく生成された test プロパティもまた、オブジェクトそのものが削除されるのですから、もちろん削除されます

そしてそれから再度 [[Get]] が呼び出されると、プロパティアクセサはもう一度新しいラッパーを生成します。もちろん、これには test プロパティのようなものは一切含まれません

// a.test: を評価するアルゴリズム
 
1. wrapper = new Number(a);
2. wrapper.test; // undefined

これが、プリミティブ値のプロパティ・メソッドがプロパティの読み出し時にのみ有効となる仕組みです。従ってもし、プリミティブ値がしばしばプロパティにアクセスするのであれば、時間リソースを節約するために、予め直接オブジェクト表現に変換しておくと良いでしょう。それに対し、値が小さな計算にのみ参加し、プロパティへのアクセスを要求しないようであれば、プリミティブ値のまま取り扱うのが効率的です。

継承

既にご存じの通り、 ECMAScript はプロトタイプに基づく委譲による継承を利用しています。

プロトタイプをつなげたものが、これも既に触れたプロトタイプチェーンを形成します。

実際には、委譲を実装する働きや、プロトタイプチェーンの分析は、記述した [[Get]] メソッドの働きに集約されます。

[[Get]] メソッドのシンプルなアルゴリズムを完全に理解していれば、 JavaScript における継承に関する疑問は自ずと消失するするでしょうし、その答えも明確になるでしょう。

しばしばフォーラム等において JavaScript の継承に関する話題が上ると、私は例として、完全にかつ正確に ECMAScript のオブジェクト構造を記述し、委譲ベースの継承について示してくれるたった一行の ECMAScript のコードを提示します。実に、コンストラクタやオブジェクトを一切生成できなくとも、言語そのものに継承が浸透しているのです。この一行のコードはとてもシンプルです。

alert(1..toString()); // "1"

今や私たちは、 [[Get]] メソッドのアルゴリズムとプロパティアクセサを理解していますから、このコードで何が起こるかを理解できるでしょう。

  1. まず、プリミティブ値 1 から、ラッパーオブジェクト new Number(1) が生成されます。
  2. そして、継承されたメソッド toString がこのラッパーによって呼び出されます。

なぜ継承が利用されるのでしょうか?。それは ECMAScript のオブジェクトは自身のプロパティを持つことができますが、この場合、ラッパーオブジェクトが自身の toString メソッドを持たないためです。従ってプロトタイプから継承することになります。 Number.prototype からです。

シンタックスの微妙なケースについて注意して下さい。上記の例の2つのドットはエラーではありません。最初のドットは数字の小数点であり、二つ目のドットはご存じプロパティアクセサです。

1.toString(); // シンタックスエラー!
(1).toString(); // OK
1..toString(); // OK
1['toString'](); // OK
プロトタイプチェーン

このプロトタイプチェーンというものを、ユーザ定義オブジェクトにおいてどのように生成するのか見てみましょう。とても簡単です。

function A() {
  alert('A.Call activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身の)、 20 (継承された)
 
function B() {}
 
// プロトタイプチェーン生成の
// 最も簡単な方法は、子のプロトタイプを
// 親コンストラクタによって新しく生成したオブジェクトに
// セットすることです。
B.prototype = new A();
 
// .constructor プロパティを修正します
// そうしなければ A を指してしまいます
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10 、 20 、共に継承
 
// Get b.x:
// b.x (無) -->
// b.Prototype.x (有) - 10
 
// Get b.y
// b.y (無) -->
// b.Prototype.y (無) -->
// b.Prototype.Prototype.y (有) - 20
 
// ここでは、 b.Prototype === B.prototype であり
// b.Prototype.Prototype === A.prototype である

このアプローチには二つの特徴があります。

一つ目は、 B.prototypex プロパティを持つことです。一見すると、これは正しくないように見えるかもしれません。 x プロパティは A にて自身のものとして定義されていて、これを継承する B コンストラクタのオブジェクトが、自身のものとして持つことを期待してしまいます。

しかし、プロトタイプ継承の場合、これは特に不思議の無い状況です。子オブジェクトが、もし自身のものとしてプロパティを持たなければ、それをプロトタイプに委譲します。この背景にあるのは、 B コンストラクタによって生成されたオブジェクトが x プロパティを持つ必要が無いという考え方です。これに対してクラスベースモデルでは、全てのプロパティは子クラスにコピーされます。

ただしもし、どうしても(クラスベースのアプローチを模倣して) x プロパティを自身のものとして B コンストラクタのオブジェクトに持たせたい場合には、いくつかのテクニックがあります。その人は後ほどご紹介します。

二つ目は、これは特徴と言うよりむしろ欠点なのですが、コンストラクタの(初期化)コードが、子プロトタイプを生成するときにも実行されてしまうと言うことです。これは、上記サンプルコードを実行した際に、 "A.[[Call]] activated"二度表示されることに見て取れます。 A コンストラクタによって生成されたオブジェクトが B.prototype として使われると共に、もちろん a オブジェクトを生成するときも使われるのです!

より問題となる例としては、親コンストラクタで投げられる例外があります。おそらく、コンストラクタによって実際のオブジェクトを生成する際には、こうしたチェックが必要だと思います。しかし明らかに、このコストラクタから生成する親オブジェクトを子のプロトタイプとして使おうとすると、同じケースは受け入れがたいものになります。

function A(param) {
  if (!param) {
    throw '引数が必要です';
  }
  this.param = param;
}
A.prototype.x = 10;
 
var a = new A(20);
alert([a.x, a.param]); // 10 、 20
 
function B() {}
B.prototype = new A(); // エラー

さらに、親コンストラクタ中で重たい計算を行う場合も、このアプローチの欠点であると捉えることができるでしょう。

こうした「特徴」や問題を解決するために、今日のプログラマは下記のようなプロトタイプを結びつける標準的なパターンを用います。このトリックの主な目的は、必要とされるプロトタイプ同士を結びつける、中間ラッパーコンストラクタを生成することにあります。

function A() {
  alert('A.Call activated');
  this.x = 10;
}
A.prototype.y = 20;
 
var a = new A();
alert([a.x, a.y]); // 10 (自身のもの)、 20 (継承で)
 
function B() {
  // あるいは単に A.apply(this, arguments) とする
  B.superproto.constructor.apply(this, arguments);
}
 
// 継承:空の中間コンストラクタを使って
// プロトタイプ間を結びつける
var F = function () {};
F.prototype = A.prototype; // 参照
B.prototype = new F();
B.superproto = A.prototype; // 親プロトタイプへの明示的な参照のための「シュガー」
 
// コンストラクタプロパティを修正する
// そうしなければ A となってしまう
B.prototype.constructor = B;
 
var b = new B();
alert([b.x, b.y]); // 10 (自身のもの)、 20 (継承で)

b インスタンスに自身のプロパティ x を生成している過程に注目して下さい。親コンストラクタを、新しく生成されたオブジェクトのコンテキストの中で、 B.superproto.constructor 参照を通じて呼び出しています。

さらに、子プロトタイプの生成のために、親コンストラクタの不要な呼び出しが発生する問題も解決しています。このアプローチでは、 "A.[[Call]] activated" のメッセージは本当に必要な時しか表示されません。

プロトタイプチェーンを作るために同じこと(中間コンストラクタの生成、 superproto シュガーの設定、オリジナルの constructor の回復)を都度実行することを避けるために、このテンプレートは便利なユーティリティ関数にまとめることができます。目的は、具体的なコンストラクタの名前に関わらず、プロトタイプを結びつけられるようにすることです。

function inherit(child, parent) {
  var F = function () {};
  F.prototype = parent.prototype
  child.prototype = new F();
  child.prototype.constructor = child;
  child.superproto = parent.prototype;
  return child;
}

これによって継承は次のようになります。

function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A); // プロトタイプを結びつける
 
var b = new B();
alert(b.x); // 10 、 A.prototype 中に見つかる

こうしたラッパーには、シンタックス的に多くのバリエーションがあります。しかし、それらは全て上記のような作用に集約されると思います。

例えば、このラッパーを最適化し、中間コンストラクタを外に出すことで(従って生成されるのはたった一つの関数になることで)再利用できます。

var inherit = (function(){
  function F() {}
  return function (child, parent) {
    F.prototype = parent.prototype;
    child.prototype = new F;
    child.prototype.constructor = child;
    child.superproto = parent.prototype;
    return child;
  };
})();

オブジェクトの本当のプロトタイプは [[Prototype]] プロパティですから、中間ラッパーである F.prototype は簡単に変更でき、再利用しても問題ありません。なぜなら new F を通じて生成される child.prototype は、その生成時に [[Prototype]]parant.prototypeその瞬間の値から受け取るからです。

function A() {}
A.prototype.x = 10;
 
function B() {}
inherit(B, A);
 
B.prototype.y = 20;
 
B.prototype.foo = function () {
  alert("B#foo");
};
 
var b = new B();
alert(b.x); // 10 、 A.prototype に見つかる
 
function C() {}
inherit(C, B);
 
// そして "superproto" シュガーを使えば、
// 同名の親メソッドを呼び出すこともできる
 
C.ptototype.foo = function () {
  C.superproto.foo.call(this);
  alert("C#foo");
};
 
var c = new C();
alert([c.x, c.y]); // 10 、 20
 
c.foo(); // B#foo, C#foo

ES5 は、このユーティリティ関数をより良いプロトタイプ連結のために標準化しました。それが Object.create メソッドです。

ES3 においても、このメソッドの簡易化されたバージョンを次のように実装できるでしょう。

Object.create ||
Object.create = function (parent, properties) {
  function F() {}
  F.prototype = parent;
  var child = new F;
  for (var k in properties) {
    child[k] = properties[k].value;
  }
  return child;
}

使用例

var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10 、 20

詳細はこの章(未訳)を参照ください。

今存在する「 JS におけるクラス的継承」の模倣のバリエーションは、全てこの方式に基づいています。見てきたように、実際これは「クラスベースの継承の模倣」などではなく、単なる「プロトタイプ同士を結びつけることによる、便利なコード再利用」なのです。

結論

この章は、かなり長く、そして詳細にわたるものとなりました。この資料がみなさまの役に立ち、 ECMAScript に関する疑念を解消してくれることを望みます。質問や補足などあれば、いつもの通りコメント欄で議論できます(訳注:このブログにはコメント欄がありません。いつものように、私の個人ブログにいずれ転載しますので、そちらで質問をいただければと思います)。

参考文献

英語版翻訳: Dmitry A. Soshnikov 、 と Garret Smith による補足[英語版].
英語版公開日時: 2010-03-04

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
オリジナルロシア語版公開日時: 2009-09-12

本シリーズはすべて英語版からの訳出です。

詳細 ECMA-262-3 第7章1節 OOP: 概説

おはようございます。「 Dmitry 先生、言いたい放題」のコーナーへようこそ。大形尚弘です。

今回は最長、そして ECMAScript をどっかに忘れた Dmitry 先生が、オブジェクト指向というものを一般より一段抽象化したレベルから語ります。

誰あろう私がそうなのですが、オブジェクト指向や、その他の何であれプログラミングパラダイムを学ぶとき、大抵そのパラダイムを代表する実装や言語をベースに学習します。すると、パラダイム自体への理解が、その実装の制約に縛られることが多々あると思います。

私は主に ActionScript2/3 でオブジェクト指向を学習しましたので、 JavaScript は「オブジェクト指向言語では無い」と思ってしまっていたことがあります。これはこの章をお読みいただければ分かるとおり、全くの間違いです。正確に表現するならば、オブジェクト指向のための「理論的糖衣」が AS3 と JavaScript で異なっている、というだけで、多くの概念は JavaScript においてもまた違った形で実装可能なのです。

とまあ、そんなことを、ごゆっくり、お時間のあるときに、腰を落ち着けて、ご理解いただければ幸いです。

第7章1節 OOP: 概説

目次

  1. はじめに
  2. 一般的な規定、パラダイム、体系
    1. クラスベース及びプロトタイプベースモデルの特徴
      1. 静的クラスベースモデル
        1. クラスとオブジェクト
        2. 階層的な継承
        3. クラスベースモデルの主要な考え方
      2. プロトタイプベースモデル
        1. 委譲ベースモデル
        2. 連結モデル
        3. ダックタイピング
        4. プロトタイプベースモデルの主要な考え方
      3. 動的クラスベースモデル
    2. さまざまな OOP 実装のその他の特徴
      1. ポリモーフィズム(多態性)
      2. カプセル化
      3. 多重継承
      4. mixin
      5. trait
      6. インタフェース
      7. オブジェクトコンポジション
      8. AOP
  3. 結論
  4. 参考文献

はじめに

この章では、 ECMAScript におけるオブジェクト指向プログラミングの主要な側面について検討していきます。「もう一つのもの( yet another )」になるべくしたものではありません、このトピックは、既に多くの記事で取り扱われています。ここでは、オブジェクト指向にまつわるさまざまな項目について、その内部から、理論的な面により注意を向けていきます。特に、オブジェクト生成のアルゴリズムについて検討し、ごく基本的な関係である継承を始めとしたオブジェクト間の関係が構築される様子について理解を深め、こうした検討の中で用いられる概念に対し正確な定義を与えていきます(そうして、 JavaScript における OOP についてしばしば取り上げられる、用語上、理論上の懸念や困惑を晴らすことを望みます)。

一般的な規定、パラダイム、体系

ECMAScript における OOP の技術的な要素を分析していく前に、 OOP の一般的な特徴を挙げ、それらの概論について中心となる考え方を明らかにする必要があるでしょう。

ECMAScript はさまざまなプログラミングパラダイムをサポートする言語です。構造化、オブジェクト指向、関数型、命令型、そしてある場合にはアスペクト指向をもサポートしています。しかし、この章ではこの中で特に OOP について検討しますので、この特性に関して、 ECMAScript に定義を与えましょう。

ECMAScript は、プロトタイプベースの実装による、オブジェクト指向プログラミング言語です。

プロトタイプベースモデルの OOP は、静的クラスベースのパラダイムとは多くの点で異なります。それらの違いを、詳しく見ていくことにしましょう。

クラスベース及びプロトタイプベースモデルの特徴

さきほどの文中で、重要な点について触れました。静的クラスベースという言葉です。ここでは「静的」という言葉によって、静的なオブジェクトとクラス、そして原則として強い型付けを意識しています(最後の点については必須ではありませんが)。

しばしば多くの記事やフォーラム中の発言において、 JavaScript は「クラス対プロトタイプ」という視点において「もう一つの」「異なった」ものと呼ばれます。しかし、この差が認められるのはいくつかの実装(動的クラスベースの Python や Ruby )におけるものであって、本質的なものではありません(いくつかの条件を受け入れれば、理論的な特徴において差異はあるにせよ、 JavaScript はそれほど「異質な」ものではないのです)。私があえて「静的」という点を強調するのは、「静的 + クラス vs 動的 + プロトタイプ」という対置こそ、本質的なものであるからです。まさに、静的及びクラスの組み合わせ(例えば C++ や Java )とそれに関係するプロパティ・メソッド解決のメカニズムこそが、プロトタイプベースの実装との正確な差異を明らかにしてくれるのです。

とはいえ、一つ一つ見ていくことにしましょう。これらのパラダイムの一般的な理論と、主要な考え方について検討しましょう。

静的クラスベースモデル

クラスベースモデルには、クラスと、そのクラスという分類に属するインスタンスという考え方があります。あるクラスのインスタンスは、オブジェクトまたは exemplar (典型) と呼ばれることもあります。

クラスとオブジェクト

クラスは、一般化されたインスタンスの特性(オブジェクトに関する情報)の形式的で抽象的な集合を表すものです。

この場合の集合とは数学のそれに近い言葉ですが、それをまたは分類と呼ぶこともできます。

例です(以下は擬似コードです)(訳注:擬似コードは背景色を変えてあります)

C = Class {a, b, c} // クラス C 、 a ・ b ・ c という特性を持つ

インスタンスの特性とは、プロパティ(オブジェクトの説明)及びメソッド(オブジェクトの活動)です。

それらの特性自身もオブジェクトとして扱うことが可能です。プロパティが書き換え可能なのか( writable )、設定可能なのか( configurable )、動作可能なのか( getter/setter )などといったものが挙げられます。

つまり、オブジェクトとは状態(クラスに記述された全てのプロパティの具体的な値)を保管するものであり、クラスは完全で不変な構造(あるプロパティの有無)と完全で不変なふるまい(あるメソッドの有無)を定義するものです。

C = Class {a, b, c, method1, method2}
 
c1 = {a: 10, b: 20, c: 30} // クラス C のオブジェクト c1
c2 = {a: 50, b: 60, c: 70} // 同じくクラス C の、また別の状態を持ったオブジェクト c2
階層的な継承

コードの再利用性を高めるために、クラスは他のクラスを拡張( extend )し、必要な追加を行うことができます。このメカニズムは、(階層的な)継承と呼ばれます。

D = Class extends C = {d, e} // {a, b, c, d, e}
d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}

インスタンスからメソッドを呼び出す際のメソッドの解決は、そのメソッドの存在に関し、完全で不変で連続的なクラスへの検査によって行われます。ネイティブの(そのインスタンスが直接に属する)クラスにそのメソッドが見つからなかった場合は、そのクラスの親、さらにそのクラスの親の親へと、完全な階層的チェーンの中で探索が継続していきます。その継承チェーンの基となるクラスまでメソッドが解決されないままである場合、結論は、そのオブジェクトは(それ自身の特性の集合、及び階層的なチェーンの中に)求められたふるまいを持たず、望まれた結果を得ることは不可能であるということになります。

d1.method1() // D.method1 (無) -> C.method1 (有)
d1.method5() // D.method5 (無) -> C.method5 (無) -> 結果無し

継承においてクラスの子孫へと複製されず、階層を形成するメソッドに対し、プロパティは常に複製されます。このふるまいは、その祖先がクラス C であるクラス D の例で見ることができます。プロパティ abc は複製され、 D の構造は {a, b, c, d, e} となります。しかし、 {method1, method2} はコピーされず、継承されます。従って、この面におけるメモリ使用量は階層の深さに比例することになります。ここにおける基本的な欠点は、階層の深いレベルにおいてオブジェクトにとって必要の無いプロパティがあったとしても、ともかくそのオブジェクトはその不要なプロパティを持ってしまう、ということです。

クラスベースモデルの主要な考え方

以上の通り、クラスベースモデルの主要な考え方は次の通りとなります。

  1. オブジェクトを生成するためには、まず初めに、必ずそのクラスを定義しなければならない。
  2. 従って、オブジェクトはそれ自身の分類、「像と肖」(構造とふるまい)において生成される。
  3. メソッドの解決は、継承の完全で間に無関係なものを挟まない不変のチェーンにおいて行われる。
  4. クラスの子孫(そしてそこから生成されたオブジェクト)は、継承チェーンのプロパティ全てを持つ(たとえそのプロパティのいくつかが具体的に継承されたクラスにおいて必要なかったとしても)。
  5. 一度生成されてしまうと、クラスは(その静的なモデルに基づき)そのインスタンスの特性(プロパティ、メソッド)の集合を変更することはできない。
  6. インスタンスは(再度その静的なモデルに基づき)、追加の自分自身のための(固有な)ふるまいや、そのクラスの構造とふるまいとは異なる追加のプロパティを持つことはできない。

それでは次に、プロトタイプに基づいた別の OOP モデルがどのように提供されるのか、見てみることにしましょう。

プロトタイプベースモデル

プロトタイプベースモデルにおいて基本となる考え方は、動的で可変なオブジェクトです。

可変性(完全な交換可能性。値だけで無く全ての特性に関して)こそが、言語の動的性に直接に関係します。

このようなオブジェクトは独自に全ての特性(プロパティ、メソッド)を保管し、クラスを必要としません。

object = {a: 10, b: 20, c: 30, method: fn};
object.a; // 10
object.c; // 30
object.method();

さらに、その動的性に基づき、オブジェクトはその特性を簡単に変更(追加、削除、修正)することができます。

object.method5 = function () {...}; // 新しいメソッドを追加する
object.d = 40; // 新しいプロパティ "d" を追加する
delete object.c; // プロパティ "c" を削除する
object.a = 100; // プロパティ "a" を修正する
 
// 結果として object: {a: 100, b: 20, d: 40, method: fn, method5: fn};

すなわち、代入時にそのオブジェクト中に代入しようとする対象の特性が存在しなければ、その特性は新たに生成され渡された値によって初期化されます。存在した場合は、上書きされます。

この場合のコードの再利用性は、クラスを拡張することではなく(ここではそもそも、不変的な特性の集合としてのクラスに一切触れていません。クラスは一切存在しないのです)、いわゆるプロトタイプを参照することによって行われます。

プロトタイプは、他のオブジェクトの原本として使われたり、あるいはあるオブジェクトが必要とする特性をそれ自身に持っていなかった際に特性の委譲先として参照する、補助的なオブジェクトです。
委譲ベースモデル

どんなオブジェクトも、他のオブジェクトのプロトタイプとして使用されることができ、そしてまたその可変性に基づき、オブジェクトはそのプロトタイプすらランタイムに動的に変更することができます。

注意していただきたいのは、ここでは私たちは一般的な理論について検討しているのであり、具体的な実装には触れていません。具体的な実装、特に ECMAScript について検討する際には、それらの実装の多くの特徴について見ていくことになるでしょう。

例です(擬似コード)

x = {a: 10, b: 20};
y = {a: 40, c: 50};
y.Prototype = x; // x は y のプロトタイプ
 
y.a; // 40 、 y 自身の特性
y.c; // 50 、これも y 自身の特性
y.b; // 20 - これはプロトタイプから得られたもの: y.b (無) -> y.Prototype.b (有): 20
 
delete y.a; // 自身の特性 "a" を削除する
y.a; // 10 - プロトタイプから得られたもの
 
z = {a: 100, e: 50}
y.Prototype = z; // y のプロトタイプを z に変更した
y.a; // 100 - プロトタイプから得られたもの
y.e; // 50 - これもまたプロトタイプから得られたもの
 
z.q = 200 // プロトタイプに新たなプロパティを追加する
y.q // その変更結果は y からも利用可能

この例は、プロトタイプがある特性に関するヘルパーオブジェクトとして利用される際の、プロトタイプに関する重要な特徴とメカニズムを示しています。ある特性が対象のオブジェクトに存在しない場合は、他のオブジェクトがそれを代理できるのです。

このメカニズムは委譲と呼ばれ、またはそのプロトタイプ的モデルに基づき、委譲プロトタイピング(または委譲ベースのプロトタイピング)と呼ばれます。

この場合の特性への参照は、オブジェクトにメッセージを送ると表現され得ます。対象のオブジェクトが自身でそのメッセージに応答することができない場合、オブジェクトはそのプロトタイプに処理を委譲します(メッセージに応えることを試みるように依頼します)。

同様にこの場合のコード再利用性は、委譲ベースの継承、またはプロトタイプベースの継承と呼ばれます。

どんなオブジェクトもプロトタイプになることができるということは、プロトタイプもまた、それ自身のプロトタイプを持つことができるということを意味します。この連結されたプロトタイプの組み合わせが、いわゆるプロトタイプチェーンを形成します。このチェーンは静的クラスモデルにおける階層性に似ていますが、ここではその可変性によって、簡単に再配置し、階層や構造を変更することができるのです。

x = {a: 10}
 
y = {b: 20}
y.Prototype = x
 
z = {c: 30}
z.Prototype = y
 
z.a // 10
 
// z.a はプロトタイプチェーン中に見つかる
// z.a (無) ->
// z.Prototype.a (無) ->
// z.Prototype.Prototype.a (有): 10

もし対象のオブジェクト及びそのプロトタイプチェーンが送られたメッセージに応答できなかった場合、そのオブジェクトは対応するシステムシグナルを発生します。それにより、送出を継続することが可能か、他のチェーンに委譲することができるかを判断します。

このシステムシグナルは、動的クラスベースシステムを含め多くの実装で利用可能です。 SmallTalk では #doesNotUnderstand 、 Ruby では method_missing 、 Python では __getattr__ 、 PHP では __call 、 ECMAScript 実装の一つでは __noSuchMethod__ などです。

例です( SpiderMonkey ECMAScirpt 実装)。

var object = {

  // メッセージに応答できないことに関する
  // システムシグナルを捕獲
  __noSuchMethod__: function (name, args) {
    alert([name, args]);
    if (name == 'test') {
      return '.test() method is handled';
    }
    return delegate[name].apply(this, args);
  }
 
};
 
var delegate = {
  square: function (a) {
    return a * a;
  }
};
 
alert(object.square(10)); // 100
alert(object.test()); // .test() メソッドがうまく処理された

つまり、静的クラスベースの実装に対し、メッセージに応答不可能な場合の結論はこうです。その時点における対象のオブジェクトは、必要とされる特性を持っていない。しかし、代替のプロトタイプチェーンを分析したり、あるいはオブジェクトがいくつかの変更の後にその特性を持ちえる場合に、望まれた結果を得ることは依然として可能であるということなのです。

ECMAScript に関して言えば、まさにこの実装、委譲ベースのプロトタイピングが用いられています。しかし、後に見ていくように、仕様及び実装によってそれぞれ独自の特徴があります。

連結モデル

公正を期すために、別の( ECMAScript では使われていませんが)ケースについても定義から少々触れておかないとなりません。プロトタイプが、他のオブジェクトがそこから特性を複製する、原本としてふるまうというモデルです。

ここでのコードの再利用性は、委譲では無く、オブジェクトの生成時におけるプロトタイプの完全な複製(クローン)によるものとなります。

この種のプロトタイピングは、連結型プロトタイピングと呼ばれます。

プロトタイプの特性の全てを自身にコピーした後、オブジェクトは以降そのプロパティとメソッドを完全に変更することができ、これはプロトタイプもまた同様です(そしてこのプロトタイプへの変更は、既に存在するオブジェクトには影響しません。委譲ベースモデルにおいてプロトタイプの特性を変更した場合とは異なります)。このようなアプローチの利点は送出と委譲にかかる時間の削減であり、欠点はより高いメモリ使用量です。

ダックタイピング

静的クラスベースモデルと比較した場合の、動的で、弱い型付け、オブジェクトの可変性に話を戻すと、オブジェクトがある活動を取り得るかどうかの検査を通過するということは、オブジェクトがどんな型(クラス)に属しているかではなく、そのメッセージに応答することができるか(検査の後実際にオブジェクトが必要とされる活動ができるか)に関係します。

// 静的クラスベースモデルでは
if (object instanceof SomeClass) {
  // ある活動が許可される
}

// 動的な実装では
// その瞬間のオブジェクトの型は必須では無い
// なぜなら可変性によって、型や特性は変換可能だからである
// 繰り返しになるが、本質的なことは
// オブジェクトが "test" メッセージに
// 応答できるかどうかということである
 
if (isFunction(object.test)) // ECMAScript
 
if object.respond_to?(:test) // Ruby
 
if hasattr(object, 'test'): // Python

プログラミング用語ではこれはダックタイピングと呼ばれるものです。つまりオブジェクトが、階層中のオブジェクトの位置やある具体的な型に属することではなく、検査のその瞬間のある特性の集合によって識別されるのです。

プロトタイプベースモデルの主要な考え方

このアプローチの主な特徴についてまとめてみましょう。

  1. 考え方の中心となるのは、オブジェクトである。
  2. オブジェクトは完全に動的で、可変である(そして理論的にはある型から他の型への変異も可能である)。
  3. オブジェクトはその構造とふるまいを記述する厳格なクラスを持たない。オブジェクトはクラスを必要としない。
  4. しかし、クラスは持たないが、オブジェクトは受け取ったメッセージに自身で応えることができなかった場合に委譲できる、プロトタイプを持っている。
  5. オブジェクトのプロトタイプは、ランタイムにいつでも変更することができる。
  6. 委譲ベースモデルでは、プロトタイプの特性への変更は、このプロトタイプに結びつく全てのオブジェクトに影響する。
  7. 連結型プロトタイプモデルでは、プロトタイプは原本であり、他のオブジェクトはそこから複製された後、完全に独立である。プロトタイプの特性への変更は、そこから複製されたオブジェクトには影響しない。
  8. もしメッセージに応答できなかった場合も、呼び出し元に対し他の対策(例えばメッセージの送信先を変更するなど)を採るようシグナルを送出することができる。
  9. オブジェクトの識別は、その階層や具体的な型への所属ではなく、その瞬間の特性の集合によって行われる。

まだ一方、もう一つ、考慮しなければならないモデルがあります。

動的クラスベースモデル

このモデルは、最初に触れた例、つまり「クラス vs プロトタイプ」の対置は本質的では無い(特にプロトタイプチェーンが不変である場合、もっと正確な区別では、クラスにおける静的性を検討する必要がある)ということを、例示するために検討していきます。例えば、 Python や Ruby (あるいはその他の同様の言語)を取り上げてみます。これらの言語は、共に動的クラスベースのパラダイムを採用しています。しかし、ある面では、プロトタイプベースの実装の特徴も、いくつかその中に見て取ることができます。

下記の例では、まさに委譲ベースのプロトタイピングのように観察することができます。クラス(プロトタイプ)を拡張し、その変更がこのクラスに結びつく全てのオブジェクトに影響します。さらにまた、動的に、ランタイムに、オブジェクトのクラスを変更すること(委譲のための新しいオブジェクトを与えることで)などが可能なのです。

# Python
 
class A(object):
 
    def __init__(self, a):
        self.a = a
 
    def square(self):
        return self.a * self.a
 
a = A(10) # インスタンスを生成
print(a.a) # 10
 
A.b = 20 # クラスの新しいプロパティ
print(a.b) # 20 - インスタンス "a" からの「委譲」を通じて利用可能
 
a.b = 30 # 自身のプロパティを生成
print(a.b) # 30
 
del a.b # 自身のプロパティを削除
print(a.b) # 20 - 再度、クラス(プロトタイプ)から取られる

# プロトタイプベースモデルと同様
# オブジェクトの「プロトタイプ」を
# ランタイムに変更することができる
 
class B(object): # 「空っぽの」クラス B
    pass
 
b = B() # クラス B のインスタンス
 
b.__class__ = A # クラス(プロトタイプ)を動的に変更する
 
b.a = 10 # 新しいプロパティを生成
print(b.square()) # 100 - クラス A のメソッドを利用可能
 
# クラスへの明確な参照を削除することもできる
del A
del B

# しかしオブジェクトは引き続き
# 暗黙の参照を持っており、メソッドは依然利用可能
print(b.square()) # 100

# しかし、クラスをビルトインのものに
# 変更することはできない(現在のバージョンでは)
# これは実装の特徴である
b.__class__ = dict # error

Ruby でも状況は似ています。完全に動的なクラスが使われ(ちなみに、現在のバージョンの Python では、 Ruby や ECMAScript と異なり、ビルトインのクラス・プロトタイプを拡張することはできません)、完全にオブジェクトやクラスの特性を変更できます(クラスにメソッドやプロパティを追加すると、これらの変更は既に存在するオブジェクトに影響します)。しかしながら、例えば Ruby では、オブジェクトのクラスを動的に変更することはできません。

とはいえ、このシリーズは Python や Ruby のためのものではありませんので、この比較はここまでとし、 ECMAScript そのものに議論に進んでいきたいと思います。

しかしその前に、さらに別の、ある OOP 実装では利用可能であるような「シンタックス及び理論のシュガー」について見てみなくてはなりません。これに関する疑問はしばしば JavaScript についての記事に現れてくるからです。

そしてこの項では特に、「 JavaScript は、クラスでは無くプロトタイプを持つ異なる言語だ」といったような言葉の誤りを示してきました。異なるクラスベース実装間でさえ、その実装において全てが完全に異なっているわけではないということを理解する必要があります。例えもし「 JavaScript は違う」と語らざるを得ないとしても、(「クラス」の考え方だけでなく)関係する全ての特徴について検討する必要があると思います。

さまざまな OOP 実装のその他の特徴

この項では、さまざまな OOP 実装で見られるその他の特徴やコード再利用の種類について、 ECMAScript での OOP 実装と比較しながら、簡単に見ていきます。この理由は、 JavaScript に関する記事の中で、本来さまざまな実装が可能であるにもかかわらず、 OOP の考え方がほんのいくつかの習慣的な実装に限られてしまっているためです。ただ一つ(主として)の要件は、技術的、そして理論的に証明され得るということだけのはずです。ある(習慣的な) OOP 実装による「シンタックスシュガー」に類似性を見いだせなければ、 JavaScript は短慮にも「純粋な OOP 言語では無い」と呼ばれてしまうことになります。これは誤った考えです。

ポリモーフィズム(多態性)

ECMAScript のオブジェクトは、いくつかの意味で多態的です。

まず例として、一つの関数を、まるでもともとそのオブジェクトの特性であったかのように、異なるオブジェクトに適用することができます(なぜなら this 値は実行コンテキストへの進入時に決定されるためです)。

function test() {
  alert([this.a, this.b]);
}
 
test.call({a: 10, b: 20}); // 10, 20
test.call({a: 100, b: 200}); // 100, 200
 
var a = 1;
var b = 2;
 
test(); // 1, 2

しかしながら、これには例外もあります。例えば Date.prototype.getTime() メソッドは、仕様によれば this 値は常に date オブジェクトで無ければなりません。そうでは無い場合は例外が発生します。

alert(Date.prototype.getTime.call(new Date())); // time
alert(Date.prototype.getTime.call(new String(''))); // TypeError

次に、関数が全てのデータ型に対して等しく定義される、いわゆるパラメトリック多態は、多態的な関数型の引数を受け入れます(例えば配列の .sort メソッドの引数は、多態的なソート関数です)。ちなみに、二つ前の例もまた、パラメトリック多態の一種であると見なすことができます。

そしてまた、プロトタイプにおいてメソッドを空のまま定義し、生成される全てのオブジェクトにおいてそのメソッドを再定義(実装)することもできます(つまり、「一つのインタフェース(シグネチャ)に対したくさんの実装」を実現できます)。

多態性とはまた、前述したダックタイピングと関係づけられます。つまりオブジェクトの型と階層における位置は重要では無く、必要な特性を持っていさえすれば簡単に受け入れられるのです(つまり繰り返しになりますが、外観的なインタフェースのみが重要で、実装は異なって構わないということです)。

カプセル化

この概念は、しばしば受け取り方において混乱や誤りが見受けられるものの一つです。ここでは、いくつかの OOP 実装における便利な「シュガー(糖衣性)」の一つ、よく知られる修飾子である、オブジェクトの特性に対するアクセスレベル(またはアクセス修飾子)、すなわち privateprotected 、そして public といったものついて検討します。

私がここで強調しておきたいのは、カプセル化の本来の目的は、抽象性を高めることであり、「あなたのクラスのフィールドに直接何かを書き込もうとする悪意のあるハッカー」から偏執的にフィールドを隠そうとすることでは無い、ということです。

隠す目的のために隠すというのは、大きな(そして蔓延した)誤解です。

アクセスレベル( private 、 protected 、 public )は、より抽象的に記述し、より抽象的なシステムを構築するためのプログラマの利便性のために、いくつかの OOP 実装で導入されてきました(そして十分に便利な「シュガー(糖衣性)」を持っています)。

この特徴は、いくつかの実装(既に触れた Python や Ruby )に見ることができます。( Python の場合)まずある一方では __private_protected なプロパティがあり(先頭のアンダースコアによる命名の運用によって特定されます)、これらは外側からアクセスすることはできません。もう一方では、 Python はこのようなフィールドの別名を持っており( _ClassName__field_name )、この名前によって、 外側からアクセスすることができます

class A(object):
 
    def __init__(self):
      self.public = 10
      self.__private = 20
 
    def get_private(self):
        return self.__private
 
# 外側
 
a = A() # A のインスタンス
 
print(a.public) # OK 、 30
print(a.get_private()) # OK、 20
print(a.__private) # 失敗、 A の記述の内部からのみ利用可能

# しかし Python はこうしたプロパティの別名を持つ
# _ClassName__property_name
# この名前によって、これらのプロパティは
# クラスの外側においても利用可能
 
print(a._A__private) # OK 、 20

一方 Ruby においては、 private や protected な特性を定義する仕組みがあります。そして同時に、特別なメソッド( instance_variable_getinstance_variable_setsend など)がカプセル化されたデータへのアクセスを可能にします。

class A
 
  def initialize
    @a = 10
  end
 
  def public_method
    private_method(20)
  end
 
private
 
  def private_method(b)
    return @a + b
  end
 
end
 
a = A.new # 新しいインスタンス
 
a.public_method # OK 、 30
 
a.a # 失敗、 @a はゲッタ "a" を持たないプライベートなインスタンス変数

# 失敗
# "private_method" はプライベートで、
# クラス A の定義中からのみ利用可能
 
a.private_method # エラー

# しかし特別なメタメソッドを使えば
# それらカプセル化されたデータにアクセス可能
 
a.send(:private_method, 20) # OK 、 30
a.instance_variable_get(:@a) # OK 、 10

この主な動機は、プログラマ自身が、カプセル化された(私が注意して「隠蔽された」という言葉を避けていることにご注意ください)データにアクセスしたいのだということです。もしこれらのデータが不正に変更されたり、エラーが発生したら、その全ての責任は全くプログラマ自身にあります。これは単なる「タイポ」や「誰かが不用意にフィールドを変更してしまった」といったものとは性質が違います。しかし、もしこのようなケースが頻繁に起こるとすれば、これは悪いプログラミング習慣、悪いプログラミングスタイルと言わざるを得ないでしょう。一般的には、オブジェクトとは public な API を通じて「話し合う」ことが最善であるからです。

何度も繰り返しますが、カプセル化の基本的な目的は、補助的なヘルパーデータのユーザからの抽象化であって、「ハッカーからオブジェクトを守る」ことではありません。ソフトウェアのセキュリティや安全のためには、「 private 」修飾子などではなく、よりもっと深刻な対策が必要です。

補助的なヘルパー(ローカル)オブジェクトをカプセル化することで私たちは、未来にわたるパブリックインタフェースのふるまいの変更を最小のコストで実現し、これらの変更の箇所を予測し、局所化するのです。そしてこれこそが、カプセル化の本当の目的なのです。

setter メソッドの重要な目的もまた、難しい計算を抽象化することにあります。例えば、 element.innerHTML setter は、 innerHTML のための setter 関数の内部で難しい計算と検査が行われている一方、「それではこの要素の html は次の通りです」というように抽象化して表現できてしまいます。ここでは 抽象化 についてお話ししましたが、カプセル化が、その向上のために役立っているのです。

カプセル化の考え方というものは、特に OOP に限ったものではありません。例えば、さまざまな計算をカプセル化し、抽象性を利用できるようにする関数というものも考えられます(例えばユーザにとって、 Math.round がどのように実装されているかということはさほど重要ではありません。ユーザは単にこれを呼び出しさせすれば良いのです)。これがカプセル化というものであり、「 private 、 protected 、 public 」といった言葉は特に必要としないのです(訳注:それらはカプセル化という考え方に対する「アイデアシュガー」だということだと思います)。

現行のバージョンの ECMAScript では、 private 、 protected 、 public といった修飾子は定義されていません。

しかし実践においては、「 JS におけるカプセル化の模倣」と呼べるようなことが可能です。大抵はこの目的のために、取り囲むコンテキスト(概してコンストラクタ関数そのもの)が利用されます。不幸にも、しばしばこうした「模倣」を実装するにあたって、プログラマは全く抽象的でない要素に対し、(盲目的に)「 getter / setter 」を定義することができてしまいます(再び、これはカプセル化の誤った取り扱い方です)。

function A() {
 
  var _a; // "private" a
 
  this.getA = function _getA() {
    return _a;
  };
 
  this.setA = function _setA(a) {
    _a = a;
  };
 
}
 
var a = new A();
 
a.setA(10);
alert(a._a); // undefined 、「プライベート」
alert(a.getA()); // 10

ここでお気づきの通り、生成されたオブジェクト一つ一つに対し、 "getA/setA" のメソッドのペアが生成され、生成されるオブジェクトの数に直接に比例するメモリ使用量の問題につながります(メソッドがプロトタイプにて定義できていればそういうことは無いのですが)。とはいえ、このケースでは、理論的には結合オブジェクト邦訳)による最適化が可能ではあります。

また、さまざまな記事中においてこうしたメソッドを「特権メソッド」と呼ぶことがありますが、正確には、 ECMA-262-3 仕様には「特権メソッド」という考え方は存在していません。

しかし、コンストラクタ関数の中でオブジェクトのメソッドを生成すること自体は、いたって通常のことですし、それは言語の思想にも現れています。オブジェクトは完全に可変であり、それぞれ独自の特性を持つことができるのです(コンストラクタ中で、条件によってあるオブジェクトには追加のメソッドを持たせ、あるメソッドにはそれを持たせないということが可能です)。

さらに JavaScript に関して言えば、このような「隠蔽された」「プライベートな」変数はさほど隠蔽されている訳ではありません(カプセル化が依然誤解されていて、 setter メソッドを使わずあるフィールドに直接値を書き込もうとする「悪意のあるハッカー」から守るものだと捉えられているとするなら、という意味においてです)。いくつかの実装においては、呼び出し元コンテキストを eval 関数に渡すことによって、必要なスコープチェーン(およびその中の全ての変数オブジェクト)にアクセスすることができます(バージョン1.7までの SpiderMonkey でテストしています)。

eval('_a = 100', a.getA); // あるいは a.setA 。どちらにせよ "_a" が Scope 中に存在する
a.getA(); // 100

さらにまた、アクティベーションオブジェクトへの直接参照が可能な実装(例えば Rhino )においては、アクティベーションオブジェクトの対応するプロパティにアクセスすることで、内部変数の値を変更することが可能です。

// Rhino
var foo = (function () {
  var x = 10; // 「プライベート」
  return function () {
    print(x);
  };
})();
foo(); // 10
foo.__parent__.x = 20;
foo(); // 20

時折、組織的な手法として(これもまたカプセル化の一種として)、 JavaScript 中の「 private 」及び「 protected 」データをアンダースコアから始めるような運用を採る場合もあります(ただしこれは Python とは違い、ただの命名のための慣習でしかありません)。

var _myPrivateData = 'testString';

取り囲む実行コンテキストの効果に関して言えば、特に本当に補助的な、オブジェクトの直接関係しないような(ヘルパー)データのカプセル化のために、外部 API からそれらを抽象化する目的で、これは実に頻繁に利用されます。

(function () {
 
  // コンテキストを初期化
 
})();
多重継承

多重継承は、コードの再利用を向上する上で便利な「シュガー(糖衣)」です(一つのクラスを継承するなら、どうして10個のクラスを一度に継承しないのでしょう?)。とはいえ、多重継承には数多くの問題があり、そのために実装系ではあまり一般的ではありません。

ECMAScript は多重継承をサポートしていません(つまり、ただ一つのオブジェクトのみが、直接のプロトタイプになり得るということです)。しかし、 ECMAScript の祖先である Self プログラミング言語ではそれが可能です。ただし実装によっては、例えば SpiderMonkey では、 __noSuchMethod__ を利用して、送出を管理し、代替のプロトタイプチェーンに委譲することができます。

mixin

mixin もまた、コード再利用に関する便利な手法です。 mixin は、多重継承の代替手法として提案されてきました。どんなオブジェクトにもミックスできる独立の要素があり、それによってオブジェクトの機能を拡張するのです(従ってオブジェクトはいくつもの mixin をミックスできます)。 ECMA-262-3 仕様では「 mixin 」 の考え方は定義されていませんが、 mixin そのものの定義について考えてみれば、 ECMAScript は動的で可変のオブジェクトを持ちますから、あるオブジェクトを他のオブジェクトにミックスすることを妨げるものは何もありません。単純に、その特性を拡張できるのです。

古典的な例です。

// 拡張のためのヘルパー
Object.extend = function (destination, source) {
  for (property in source) if (source.hasOwnProperty(property)) {
    destination[property] = source[property];
  }
  return destination;
};
 
var X = {a: 10, b: 20};
var Y = {c: 30, d: 40};
 
Object.extend(X, Y); // Y を X にミックス
alert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40

ご注意いただきたいのは、私はこれらの定義(「 mixin 」「ミックスする」)を括弧付き又は斜体で示しました。これは、 ECMA-262-3 では、このような考え方が定義されているわけではなく、「ミックスする」というよりはオブジェクトを新しい特性で拡張していると捉えられるものだからです(一方、例えば Ruby では、 mixin の考え方は公式に採用されていて、あるモジュールのプロパティを単に全て別のオブジェクトに複製するのではなく、 mixin は盛り込まれるモジュールに対して参照を生成します。つまり、実際のところ委譲のための追加のオブジェクト(「プロトタイプ」)を作ることなのです)。

trait

trait は mixin に似ていますが、多くの機能を持っています(例えば基本的なものをあげると、定義によれば、 mixin では可能な、名前の衝突する状態を持つことができません)。 ECMAScript に関して言えば、 trait は mixin と同様の原理によって模倣することができます。標準仕様は「 trait 」の考え方を採用はしていません(訳注:実際に JavaScript で trait を実現する traits.js というライブラリが存在します)。

インタフェース

いくつかの OOP 実装で利用可能なインタフェースも、 mixin や trait と同様です。しかしそれらとは異なり、インタフェースはクラスに対し、あるシグネチャのメソッドのふるまいを完全に実装することを要求します。

インタフェースは、全くもって抽象クラスとして扱うことができますが、抽象クラス(一部のメソッドを実装し、残りのパートをシグネチャとしてのみ定義できる)と異なる点は、一段階の継承において、クラスがいくつものインタフェースを実装することができる点です。この点において、インタフェースは mixin 同様、多重継承の代替として用いられることができます。

ECMA-262-3 標準では「インタフェース」も「抽象クラス」の考え方も定義されてはいません。しかし模倣として、「空の」メソッドを持ったオブジェクトによって、他のオブジェクトを拡張することができます(あるいは、メソッド中で例外を投げ、このメソッドが実装されるべきであるということを警告することができます)。

オブジェクトコンポジション

オブジェクトコンポジションもまた、動的なコード再利用のためのテクニックの一つです。オブジェクトコンポジションが継承と異なるのは、高い柔軟性と動的に可変な委譲の構造を実装できることです。これはつまるところ、委譲ベースプロトタイピングの基礎です。動的に可変なプロトタイプの代わりに、オブジェクトが委譲のためのオブジェクトを集約し(集約される結果として、コンポジションを生成します)、以降オブジェクトにあるメッセージが送出される時に、オブジェクトはこのメッセージを集約した委譲先に委譲します。委譲先は一つに限りませんし、さらに動的な性質によってランタイムにそれらを変更することすら可能です。

既に触れた __noSuchMethod__ もこの例の一つなのですが、明確に委譲を使用する例を別に挙げてみましょう。

例です。

var _delegate = {
  foo: function () {
    alert('_delegate.foo');
  }
};
 
var agregate = {
 
  delegate: _delegate,
 
  foo: function () {
    return this.delegate.foo.call(this);
  }
 
};
 
agregate.foo(); // foo を委譲する
 
agregate.delegate = {
  foo: function () {
    alert('foo from new delegate');
  }
};
 
agregate.foo(); // 新しい委譲先の foo

このようなオブジェクトの関係を、「 has-a 」と呼び、継承による「 is-a 」関係が「子孫である」のに対し、「包括している」ということになります。

(継承と比較して柔軟性があることに対し)明確なコンポジションの欠点は、中間(媒介)コードの量が大きくなりがちだということです。

AOP

アスペクト指向プログラミングの特徴の一つとして、関数デコレータが挙げられるでしょう。 ECMA-262-3 仕様では「関数デコレータ」の考え方について明確な定義はありませんが(対して Python では公式な用語です)、関数型の引数を持つことから、関数はデコレートされることができ、(いわゆるアドバイスを採用することによって)あるアスペクトに関して実行されることができるのです。

簡単なデコレータの例を見てみましょう。

function checkDecorator(originalFunction) {
  return function () {
    if (fooBar != 'test') {
      alert('wrong parameter');
      return false;
    }
    return originalFunction();
  };
}
 
function test() {
  alert('test function');
}
 
var testWithCheck = checkDecorator(test);
var fooBar = false;
 
test(); // 'test function'
testWithCheck(); // 'wrong parameter'
 
fooBar = 'test';
test(); // 'test function'
testWithCheck(); // 'test function'

結論

ここまでで、 OOP に関する概説を終えたいと思います(この資料がみなさまにとって有益であることを望みます)。そして、『第7章2節 OOP: ECMAScript での実装』へと続きます。

参考文献

英語版翻訳: Dmitry A. Soshnikov 、 Juriy "kangax" Zaytsev の助力を借りて[英語版].

英語版公開日時: 2010-03-04

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

オリジナルロシア語版公開日時: 2009-09-12

本シリーズはすべて英語版からの訳出です。



詳細 ECMA-262-3 第6章 クロージャ

おつかれさまでございます。東洋大学柏原選手の好きな声優は花澤香菜さんですが、株式会社ミクシィ大形選手の好きな声優は五十嵐裕美さんです。お世話になります。

さて、 Dmitry 先生の ECMA-262-3 シリーズもついに山場、クロージャの章へとやって参りました。「 JavaScript はクロージャが使えて強力」「 JavaScript 理解のキモはクロージャ」などといった売り文句や脅し文句を耳にされたことは無いでしょうか。クロージャがなぜ強力なのか、そしてそれはどのような仕組みに基づくものなのか、これは ECMAScript だけに留まらず、一般的な意味でクロージャを理解できる名章です。どうぞお時間のある時に、気持ちを落ち着けて、ごゆっくりご覧ください。

詳細 ECMA-262-3 第6章 クロージャ

目次

  1. はじめに
  2. 概説
    1. 定義
    2. Funarg 問題
    3. クロージャ
  3. ECMAScript のクロージャ実装
    1. 一つの [[Scope]] 値はみんなのもの
    2. Funarg と return
    3. 二つの見方
  4. クロージャの実際の用途
  5. 結論
  6. 参考文献

はじめに

この章では、 JavaScript に関して最も議論されているトピックの一つ、クロージャについてお話しします。実際のところこのトピックは目新しいものではありませんし、幾度となく議論され、そのエッセンスを扱った記事が既に数多く存在します(中にはとてもすばらしいものがあります。例えば、参考文献中に示す Richard Cornford 氏の記事です)。しかし、ここでまた私たちも、理論的な観点から議論し理解を深めてみることにしましょう。そして ECMAScript の内側から、クロージャがどのように作られているのかを見ていくことにしましょう。

これまでの章でも述べたように(全体に共通する話ですが)、各々の章はそれ以前の章に依存しています。必要があれば、この章を完全に理解するために、『第4章 スコープチェーン』、とおそらくはそれに関連するさらに前の章、『第2章 変数オブジェクト』を参照してください。

概説

ECMAScript におけるクロージャを検討する前に、 ECMA-262-3 の仕様とは関係無く、関数型プログラミングにおける一般的な理論の観点から、クロージャにまつわる定義を行うことにします。ただしそれらの定義を説明するサンプルには、もちろん ECMAScript を用います。

ご存じの通り、関数型言語(そして ECMAScript はこのパラダイム及びスタイルをサポートしています)においては、関数とはデータです。すなわち、変数中に保存されることができ、他の関数に引数として渡されることができ、関数から戻されることなどができるわけです。こうした関数は、特別な名前と構造を持っています。

定義

関数型の引数 / functional argument ( "Funarg" )とは、その値が関数である引数のことです。

例:

function exampleFunc(funArg) {
  funArg();
}
 
exampleFunc(function () {
  alert('funArg');
});

この場合、関数 exampleFunc に渡される実引数の匿名関数が funarg です。

翻って、 funarg を受け取る関数は、高階関数( higher-order function 略して HOF )と呼ばれます。

こうした関数には、汎関数( functional )、またはより数学的に、作用素( operator )という呼び方もあります。上の例では、関数 exampleFunc汎関数、 functional です( "functional" は形から形容詞に見えますが、この場合は名詞です)。

既に述べたように、関数は引数として渡されるだけでなく、他の関数からとして戻されることが可能です。

この、他の関数を戻す関数のことを、関数値をとる関数 / functions with functional value または関数型の関数 / function valued functionsと呼びます。
(function functionValued() {
  return function () {
    alert('returned function is called');
  };
})()();
通常のデータとして参加できる関数(実引数として渡され、関数型の引数を受け取り、関数値として戻されるなど)は、第一級関数(より一般的に第一級オブジェクト)と呼ばれています。

ECMAScript では、全ての関数が第一級オブジェクトです。

それ自身を引数として受け取る汎関数は、自己適用関数と呼ばれます。

(function selfApplicative(funArg) {
 
  if (funArg && funArg === selfApplicative) {
    alert('self-applicative');
    return;
  }
 
  selfApplicative(selfApplicative);
 
})();

自分自身を戻す関数は、自己複製関数と呼ばれます。時折、小説などでは自己増殖という言葉も使われますね。

(function selfReplicative() {
  return selfReplicative;
})();
自己複製関数の興味深い適用例の一つは、配列そのものを一度にではなく、その配列の一つ一つを取り扱う宣言型の形式です。
// 配列を受け入れる
// 命令型の関数
 
function registerModes(modes) {
  modes.forEach(registerMode, modes);
}
 
// 使用例
registerModes(['roster', 'accounts', 'groups']);

// 自己複製関数を使った
// 宣言型の形式
 
function modes(mode) {
  registerMode(mode); // 一つのモードを登録
  return modes; // そして関数そのものを戻す
}
 
// 使用例:モードを「宣言」する

modes
  ('roster')
  ('accounts')
  ('groups')
しかしながら、実践的には配列そのものを取り扱う方が、効率的で直観的です。

funarg の中で定義されたローカル変数は、もちろんその funarg のアクティベーション時にアクセスできます。なぜなら、コンテキストに進入する度に、そのコンテキストのデータを保管する変数オブジェクトが作られるからです。

function testFn(funArg) {

  // funarg のアクティベーション
  // ローカル変数 localVar にアクセス可能
  funArg(10); // 20
  funArg(20); // 30
 
}
 
testFn(function (arg) {
 
  var localVar = 10;
  alert(arg + localVar);
 
});

しかし、ご存じの通り(特に第4章で明らかにしたとおり)、 ECMAScript の関数は親関数で囲われることができ、親コンテキストの変数を利用できます。この機能には、いわゆるfunarg 問題が関係してきます。

Funarg 問題

スタック指向言語においては、関数のローカル変数はスタックに保管されます。そこには、関数のアクティベーションの度に、それらの変数や関数の仮引数が push されていきます。

その関数が return するとき、そうした変数達はスタックから削除されます。このモデルは、関数を関数値として扱う(親関数から戻すような)際に大きな制約となってしまいます。主にこの問題は、関数が自由変数を持つときに現れます。

自由変数とは、関数によって利用されながらも、その関数の引数でも、ローカル変数でも無い変数のことです。

例:

function testFn() {
 
  var localVar = 10;
 
  function innerFn(innerParam) {
    alert(innerParam + localVar);
  }
 
  return innerFn;
}
 
var someFn = testFn();
someFn(20); // 30

この例では、変数 localVarが、関数 innerFn において自由です。

もしここでのシステムが、ローカル変数の保管にスタック指向モデルを採用していたならば、関数 testFn の return 時にその全ての変数はスタックから削除されてしまっていたでしょう。そうなってしまっては、関数 innerFn の、外側からのアクティベーション時においてエラーになってしまうでしょう。

さらに、特にこのケース、スタック指向の実装の場合は、関数 innerFn を戻すこと自体が不可能です。なぜなら innerFn もまた testFn においてローカルであり、 testFn の return 時に削除されるべきものだからです。

関数オブジェクトの別の問題は、動的スコープ実装のシステムにおいて関数を実引数として渡す際に関係します。

例(擬似コード):

var z = 10;
 
function foo() {
  alert(z);
}
 
foo(); // 10 - 静的または動的スコープの両方で
 
(function () {
 
  var z = 20;
  foo(); // 10 - 静的スコープ、 20 - 動的スコープ
 
})();
 
// foo を実引数として
// 渡す際も同様
 
(function (funArg) {
 
  var z = 30;
  funArg(); // 10 - 静的スコープ、 30 - 動的スコープ
 
})(foo);

動的スコープを採用するシステムでは、変数(識別子)解決変数の動的な(アクティブな)スタックによって処理されます。従って、自由変数は関数生成時に保存された静的な(レキシカルな)スコープチェーン中ではなく、現在のアクティベーション動的チェーン中に探索されます。

ここに、曖昧さが現れます。例えばもし今 z が存在するとき(ローカル変数がスタックから削除される前の例とは異なり)、問題はこうです。関数 foo の様々な形の呼び出しに際し、 z にはどの値が使われるべきでしょうか?(どのコンテキストの?。どのスコープの?)。

次のケースが、funarg 問題の二つの種類です。関数から戻される関数値を扱うとき(上方/上向きの funarg )、関数に渡される関数型の引数を扱うとき(下方/下向きの funarg )の二種類です。

こうした問題(及びその亜種)を解決するために、クロージャの考え方が提案されました。

クロージャ

クロージャとは、コードブロックと、そのコードブロックが生成されたコンテキストのデータの組み合わせです。

擬似コードによる例を見てみましょう。

var x = 20;
 
function foo() {
  alert(x); // 自由変数 "x" == 20
}
 
// foo のクロージャ
fooClosure = {
  call: foo // 関数への参照
  lexicalEnvironment: {x: 20} // 自由変数を探索するコンテキスト
};

この例の fooClosure はもちろん擬似コードですが、とはいえ ECMAScript における関数 foo もまた、その内部プロパティの一つとして自身が生成されたコンテキストのスコープチェーンを持っているのです。

その文脈から想像され得るため、 "lexical"(レキシカル)という言葉はしばしば省略されます。このケースでは、クロージャが生成された際にそのコンテキストのデータが(同時に・その瞬間に)保存される、という点に注目します。このコードブロックが次にアクティベートされるとき、自由変数はそこに共に保存された(閉じ込められた / closured )コンテキストから探索されます。二つ前の例において、 ECMAScript では、変数 z は必ず 10 と解決されるのです。

定義では「コードブロック」という一般的な概念を用いましたが、これには大抵( ECMAScript においても)「関数」という言葉が使われますし、これを私たちも使っていきます。ただし、全ての実装系においてクロージャが関数のみに結びついているというわけではありません。例えば Ruby プログラミング言語では、クロージャは手続きオブジェクト( proc )、またはラムダ式( lambda )、あるいはコードブロックの形を取り得ます。

実装に関して言うならば、コンテキストが破壊された後もローカル変数を保存するためには、スタックベースの実装はもはや適しません(なぜならそれ自体がスタックベース構造の定義に反するためです)。従って、このようなケースでは、親コンテキストの閉じ込められた( closured )データは動的メモリアロケーション(「ヒープ」、つまりヒープベースの実装)に保存され、ガベージコレクタ( GC )参照カウントが用いられます。このようなシステムは、スピードの面ではスタックベースのシステムより非効率です。しかしながら、実装は常にこれを最適化していきます。パース時に、関数中で自由変数、関数型の引数、あるいは関数値が使われているか突き止め、それによって、データをスタック、または「ヒープ」のどちらに保存するか決定するのです。

ECMAScript のクロージャ実装

ここまで理論について議論してきましたが、やっとここで ECMAScript に直接関係するクロージャについてお話しします。まず触れておかねばならないことは、 ECMAScript は静的(レキシカル)スコープのみを用いるということです(一方、 Perl など、変数を静的あるいは動的スコープのどちらかを使用するように区別して宣言することができる言語もあります)。

var x = 10;
 
function foo() {
  alert(x);
}
 
(function (funArg) {
 
  var x = 20;
 
  // funArg にとっての変数 "x" は、
  // それが生成された(レキシカル)コンテキストから
  // 静的に保存されている
  // 従って
 
  funArg(); // 10 、 20 ではなく
 
})(foo);

理論的には、関数を生成した親コンテキストのデータは、その関数の内部 [[Scope]] プロパティに保存されています。もし関数の [[Scope]] プロパティの理解について不十分なところがあれば、是非とも Scope プロパティについて詳しくご説明している第4章に戻り、スコープチェーンについて読まれることをお勧めします。全く持って、 Scopeスコープチェーンについてしっかり理解しているならば、 ECMAScript におけるクロージャを理解する際の問題は、自ずと氷解してしまうことと思います。

関数生成のアルゴリズムについてご説明した際、 ECMAScript の全ての関数はクロージャだと述べました。なぜなら、例外なく全ての関数は、生成時に親コンテキストのスコープチェーンを保存するからです(関数がその後アクティベートされるされないに関わらず、 親のスコープ( [[Scope]] )は常に、関数生成時にその関数に対して書き込まれます)。

var x = 10;
 
function foo() {
  alert(x);
}

// 擬似コード
// foo はクロージャ
foo: <関数オブジェクト> = {
  Call: ,
  Scope: [
    global: {
      x: 10
    }
  ],
  ... // その他のプロパティ
};

上でも述べたように、最適化の目的から、関数が自由変数を使わない場合、実装系は親のスコープチェーンを保存しないかもしれません。しかし、 ECMA-262-3 仕様では、これについては何も触れられていません。従って形式上は(技術的なアルゴリズムの観点からは)、全ての関数はその生成時にスコープチェーンを [[Scope]] プロパティに保存する、と考えることができます。

実装によっては、閉じ込められた( closured )スコープに直接アクセスできるものもあります。例えば Rhino では、変数オブジェクトの章でも議論した、非標準の __parent__ プロパティが関数の [[Scope]] プロパティに相当します。

var global = this;
var x = 10;
 
var foo = (function () {
 
  var y = 20;
 
  return function () {
    alert(y);
  };
 
})();
 
foo(); // 20
alert(foo.__parent__.y); // 20
 
foo.__parent__.y = 30;
foo(); // 30
 
// 最上までスコープチェーン中を移動できる
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x); // 10

一つの [[Scope]] 値はみんなのもの

これもまたご説明しておかないとなりません。 ECMAScript における閉じ込められた( closured ) [[Scope]] は、ある一つの(同じ)レキシカルコンテキストにて生成されたクロージャ間で、同じものが共有されます。これは、あるクロージャの中で閉じ込められた変数を更新した場合、他の(同じコンテキスト中で生成された)クロージャからのこの変数の読み出しに影響するということです。

つまり、全ての内部関数は、同じ親スコープを共有しています。
var firstClosure;
var secondClosure;
 
function foo() {
 
  var x = 1;
 
  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };
 
  x = 2; // 両方のクロージャの Scope 中にある AO["x"] に影響する。
 
  alert(firstClosure()); // 3 、 firstClosure.Scope から
}
 
foo();
 
alert(firstClosure()); // 4
alert(secondClosure()); // 3

この特徴に関するよく知られたバグがあります。しばしば、ループ中で関数を生成するとき、その時のループカウンタを関数に結びつけようとして(全ての関数がそれ自身の個別に必要な値を保管することを期待して)、プログラマが期待しない結果を得ることがあります。

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

前の例がこの振る舞いを説明してくれます。関数群を生成するコンテキストのスコープは、生成される三つ全ての関数に対して、ただ一つです(全ての関数はそのスコープを Scope プロパティを通じて参照します)。つまり親スコープ中の変数 "k" は、簡単に変更され得ます。

図式的には、

activeContext.Scope = [
  ... // 高位の変数オブジェクト
  {data: [...], k: 3} // アクティベーションオブジェクト
];
 
data[0].Scope === Scope;
data[1].Scope === Scope;
data[2].Scope === Scope;

この通り、関数のアクティベート時には、最後に代入された "k" の値、つまり 3 が使われるのです。

これは、全ての変数がコード実行より前、つまりコンテキスト進入時に生成されているという事実に関係します。このふるまいは、巻き上げ(未訳)と呼ばれます

追加に囲うコンテキストを作れば、この問題を解決できます。

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      alert(x);
    };
  })(k); // "k" の値を渡す
}
 
// こうすれば正しい結果に
data[0](); // 0
data[1](); // 1
data[2](); // 2

ここで何が起こっているかを見てみましょう。

最初に、関数 _helper が生成され、直後に実引数 k を持ってアクティベートされます。

次に、関数 _helper の戻り値もまた関数であり、まさにこれがdata 配列の対応する一要素に保存されます。

このテクニックは次のような効果を持ちます。アクティベートされるとき、 _helper は都度、引数 x を持った新しいアクティベーションオブジェクトを生成し、 x の値は渡された変数 k の値になります。

従って、戻された関数の [[Scope]] プロパティは次のようになるでしょう。

data[0].Scope === [
  ... // 高位の変数オブジェクト
  親コンテキストの AO : {data: [...], k: 3},
  _helper コンテキストの AO : {x: 0}
];
 
data[1].Scope === [
  ... // 高位の変数オブジェクト
  親コンテキストの AO : {data: [...], k: 3},
  _helper コンテキストの AO : {x: 1}
];
 
data[2].Scope === [
  ... // 高位の変数オブジェクト
  親コンテキストの AO : {data: [...], k: 3},
  _helper コンテキストの AO : {x: 2}
];

このようにして、関数の [[Scope]] プロパティは必要とされる値(追加に生成されたスコープに保管される変数 x を通じて)への参照を持ちます。

戻された関数からは、引き続きもちろん変数 k にアクセスすることもできます。全ての関数に対して、正しく 3 となります。

ところで、しばしば JavaScript に関するいくつかの記事において、クロージャを上記のようなパターン、追加の関数の生成に関するものとしてのみ取り扱うような不完全な説明があります。実際的な観点から言えば、このパターンはとても重要なものです。しかしながら、理論的な観点からは、ご説明したとおり、 ECMAScript における全ての関数はクロージャです。

とはいえ、上記のパターンは唯一の解法というわけではありません。変数 "k" の必要な値を得るには、例えば、次のようなアプローチも可能です。

var data = [];
 
for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // "k" を関数のプロパティとして保存する
}
 
// この方法でも、全て正しい値を得る
data[0](); // 0
data[1](); // 1
data[2](); // 2

Funarg と return

もう一つの特徴は、クロージャからの return です。 ECMAScript では、クロージャからの return 文は、コントロールフローを呼び出し元コンテキスト( caller )に戻します。他の言語の中には、例えば Ruby においては、異なる方法で return 文を処理するさまざまな形式のクロージャが利用できます。呼び出し元に戻すものもあれば、別のケースでは、アクティブなコンテキストから完全に脱出してしまうことも可能です。

しかし ECMAScript 標準では、 return の振る舞いは次の通りです。

function getElement() {
 
  [1, 2, 3].forEach(function (element) {
 
    if (element % 2 == 0) {
      // 関数 "forEach" に戻る
      // しかし getElement からは戻らない
      alert('found: ' + element); // found: 2
      return element;
    }
 
  });
 
  return null;
}
 
alert(getElement()); // null 、 2 ではなく

しかし ECMAScript でも、アクティブコンテキストを終了したい場合はある特別な、 "break" 的な例外を throw 、 catch することで実現可能です。

var $break = {};
 
function getElement() {
 
  try {
 
    [1, 2, 3].forEach(function (element) {
 
      if (element % 2 == 0) {
        // getElement から "return"
        alert('found: ' + element); // found: 2
        $break.data = element;
        throw $break;
      }
 
    });
 
  } catch (e) {
    if (e == $break) {
      return $break.data;
    }
  }
 
  return null;
}
 
alert(getElement()); // 2

二つの見方

前述の通り、しばしばプログラマは誤ってクロージャを単なる関数から戻される内部関数であると捉えてしまいます。さらには、ただの匿名関数であると考えてしまう場合もあります。

ここでもう一度強調させてください、全ての関数(その種類に関わらず、匿名であろうと、名前付きであろうと、関数式であろうと、関数定義であろうと)は、スコープチェーンという技術によって、クロージャです

このルールの例外は、 Function コンストラクタで生成された関数で、この関数の [[Scope]]グローバルオブジェクトのみを含みます(訳注:関数の外側の環境を保存していますが、関数の直接の生成元コンテキスト(親コンテキスト)を含まないことから、区別できるものと思います)。

この曖昧さ、問題を明確にするために、 ECMAScript に関するクロージャの、二つの正しい見方を提示させてください。

ECMAScript におけるクロージャとは

  • 理論的な観点からは、全ての関数である。なぜなら全ての関数は生成時に親コンテキストのデータを保存するからである。単純なグローバル関数でさえ、グローバル変数への参照はすなわち自由変数への参照であり、このために一般的なスコープチェーン機構が用いられる。
  • 実際的な観点からは、次のような関数に特に関心が注がれる。
    • それが生成されたコンテキストが完了した後も存在する(たとえば親関数から返された内部関数)。
    • コード中で自由変数を参照する。

クロージャの実際の用途

実用面では、クロージャは的確で簡潔なデザインをもたらし、 funarg によって定義された条件に基づいてさまざまな計算をカスタマイズすることを可能にします。例として、ソート条件関数を引数として受け取る、配列の sort メソッドを挙げてみましょう。

[1, 2, 3].sort(function (a, b) {
  ... // ソート条件
});

あるいは、例として、 funarg の条件によって新しい配列を写像する汎関数である、 Array の map メソッドを見てみましょう。

[1, 2, 3].map(function (element) {
  return element * 2;
}); // [2, 4, 6]

時に、事実上無制限の条件検査を記述した funarg を用いて検索関数を実装することで、便利な手続きとなります。

someCollection.find(function (element) {
  return element.someProperty == 'searchCondition';
});

さらにまた、適用(する)汎関数、例えば funarg を配列の各要素に適用する forEach メソッドも挙げられます。

[1, 2, 3].forEach(function (element) {
  if (element % 2 != 0) {
    alert(element);
  }
}); // 1, 3

ちなみに、関数オブジェクトの applycall メソッドもまた、関数型プログラミングの適用汎関数に由来します。これら二つのメソッドについては this 値に関する項で既に議論しましたが、ここでは、これらを適用汎関数の役割を果たすものとして見ることにします。関数が、引数に適用されるのです( apply では引数の配列に対し、 call では順番に与えられる引数に対して)。

(訳注:この例が分かりづらければ、適用される funarg が主語の位置に来ていて、それを取り扱う汎関数が apply または call であるとして見直してみてください)

(function () {
  alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

その他のクロージャの重要な応用は、遅延呼び出しです。

var a = 10;
setTimeout(function () {
  alert(a); // 10, after one second
}, 1000);

そして、コールバック関数もあります。

...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
  // コールバックは、データの準備ができた後に
  // 遅れて呼び出される
  // このコールバックを生成したコンテキストが
  // 終了していても、変数 "x" は利用できる
  alert(x); // 10
};
..

さらにまた、例えば補助的なオブジェクトを隠す目的で、カプセル化されたスコープの生成もあります。

var foo = {};
 
// 初期化
(function (object) {
 
  var x = 10;
 
  object.getX = function _getX() {
    return x;
  };
 
})(foo);
 
alert(foo.getX()); // 閉じ込められた( closured ) "x" を得る - 10

結論

この章は、 ECMA-262-3 についてというよりは、一般的な理論についてご説明する方が大くなりました。しかし、この一般的な理論が、 ECMAScript におけるクロージャに関していくつかの面を明らかにし、より理解を深めることになるものと考えます。ご質問があれば、コメントで喜んでお答えします(訳注:はてなダイアリーにも転載する作業、もう少しお待ちください...)。

参考文献

英語版翻訳: Dmitry A. Soshnikov[英語版].

英語版公開日時: 2010-02-28

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

オリジナルロシア語版公開日時: 2009-07-20

本シリーズはすべて英語版からの訳出です。



詳細 ECMA-262-3 第5章 関数

あけましておめでとうございます。おつかれさまでございます。先日渋谷の、会社から徒歩5分のところにお引っ越しをし、今年は仕事に燃える、声優では五十嵐裕美さんが好きな、大形尚弘です。

さて、 ECMAScript のセオリストを目指す人々を甘く誘い、そして悟りか眠りのどちらかに確実に到達させる伝説的名著、 Dmitry 先生の ECMA-262-3 シリーズも第5章となりました。今回は関数がテーマです。

今回もまた伝説的に長くなっておりますので、気持ちが落ち着いているとき、時間のあるときに、ごゆっくりお読みいただければ、これまで正確な理由を知らず自分なりのベストプラクティスとして使っていたような書き方に明確な理由付けが与えられ、自信を持った JavaScript 生活につながっていくと思います。それではどうぞ。

詳細 ECMA-262-3 第5章 関数

目次

  1. はじめに
  2. 関数の種類
    1. 関数定義
    2. 関数式
      1. 「取り囲む括弧」に関する問題
      2. 実装による拡張: 関数文
      3. 名前付き関数式( NFE )の特徴
      4. NFE と SpiderMonkey
      5. NFE と JScript
    3. Function コンストラクタで生成された関数
  3. 関数生成のアルゴリズム
  4. 結論
  5. 参考文献

はじめに

この章では、一般的な ECMAScript オブジェクトの一つ、関数についてお話します。特に、各種関数の種類について検討し、それぞれの種類がコンテキストの変数オブジェクトにどのように影響するのか、そしてそれぞれの関数のスコープチェーンに何が含まれるのか、定義していきます。そして、「次のような生成方法で作られた関数には違いがあるのか?あるとすれば何が違うのか?」という質問についてお答えします。

これと...。

var foo = function () {
  ...
};

よくあるいつものこの形で定義された関数の違いとは何でしょうか。

function foo() {
  ...
}

また、「次のような呼び出しでは、なぜ関数自体を括弧で囲わなければならないのか?」という質問にもお答えしていきます。

(function () {
  ...
})();

この章は以前の章の内容に寄るところも多く、これまでに登場した用語を積極的に使っていきますので、この章を十分に理解するためには『第2章 変数オブジェクト』『第4章 スコープチェーン』を読むと良いでしょう。

それでは一つ一つ見ていきます。まずは関数の種類の検討から始めましょう。

関数の種類

ECMAScript には関数が三種類あり、それぞれに特徴があります。

関数定義

関数定義( Function Declaration 略して FD )は、次のような関数です。


  • 必須名を持つ。

  • ソースコード中の位置については、 Program レベルまたは他の関数の本体の中に直接現れる( FunctionBody )。(訳注: ECMAScript の構文上の構造、ソースコードの種類については、拙記事もご参照ください。)

  • コンテキスト進入時に生成される。

  • 変数オブジェクトに影響する

  • そして次の通り宣言される。

function exampleFunc() {
  ...
}

この種の関数の主な特徴は、この種類の関数だけが変数オブジェクトに影響するということです(コンテキストの VO 中に保管されます)。またこの特徴が、二つ目の重要なポイントを定義します(変数オブジェクトの性質から自然と導き出されます)。これらは、コード実行時には既に利用できるということです( FD は、コードの実行が始まる前、コンテキスト進入時に VO に積み込まれるからです)。

例(ソースコード中での定義の位置より早く関数の呼び出しを行う場合):

foo();
 
function foo() {
  alert('foo');
}

また同様に重要なポイントは、ソースコード中で関数が定義される位置です(上記関数定義の定義の2番目を見てください)。

// 関数が定義され得るのは...
// 1) グローバルコンテキストに直接
function globalFD() {
  // 2) または他の関数の本体に直接
  function innerFD() {}
}

関数が定義され得るのは、これらの二箇所しかありません(つまり、式を取る位置や、コードのブロックの中では定義できないということです)。

関数定義には、関数式と呼ばれるもう一つの選択肢があります。次にこれを見てみましょう。

関数式

関数式( Function Expression 略して FE )は、次のような関数です。

  • ソースコード中の式を取る位置でのみ定義できる。

  • 任意名を持つことができる。

  • その定義は、変数オブジェクトに影響しない

  • コード実行時に生成される。

この種の関数の主な特徴は、ソースコード中で常にの位置に現れるということです。代入式の簡単な例を見てみましょう。

var foo = function () {
  ...
};

この例は、匿名の FE が変数 foo に代入される様子を示しています。この後に、この匿名関数は foo という名前で利用可能になります。 foo() のように。

定義では、この種の関数は任意の名前を持つことができるとされています。

var foo = function _foo() {
  ...
};

ここで大切なのは、 FE の外側からは、変数 foo を通じてアクセスできる一方、関数の内側からは(例えば、再帰呼び出しのような場合に)、 _foo という名前も使うことができるということです。

一旦 FE に名前が与えられれば、 FD と見分けることは困難かもしれません。しかし、もし定義付けを知っていれば、区別することは簡単です。 FE は常に式の位置にあります。次の例では、さまざまな ECMAScript の式を見ることができます。これらの全ての関数は FE です。

// 括弧の中(グループ演算子)は式のみを取ります。
(function foo() {});
 
// 配列初期化子の中も、式のみを取ります。
[function bar() {}];
 
// カンマも式を対象に演算を行います。
1, function baz() {};

定義ではまた、 FE はコード実行時に生成され、変数オブジェクトには保管されないと示しています。この動作の例を見てみましょう。

// FE は定義の前では利用できない。
// (なぜならコード実行時に生成されるからである)
 
alert(foo); // "foo" is not defined
 
(function foo() {});
 
// 同様に、定義の後でも利用できない。
// なぜなら VO 中に存在しないからである。
 
alert(foo);  // "foo" is not defined

ここで、そもそもなぜこの種類の関数が必要なのかという疑問を持つのも当然でしょう。回答は明確です。の中で使うため、そして変数オブジェクトを「汚染しない」ためです。例えば関数を実引数として他の関数に渡すときに見て取れます。

function foo(callback) {
  callback();
}
 
foo(function bar() {
  alert('foo.bar');
});
 
foo(function baz() {
  alert('foo.baz');
});

FE が変数に代入される場合は、関数はメモリに保管、維持され、この変数名を通じて後からアクセスが可能です(なぜならご承知の通り、変数は、 VO に影響するからです)。

var foo = function () {
  alert('foo');
};
 
foo();

もう一つは、補助的なヘルパーデータを外部のコンテキストから隠すための、カプセル化されたスコープを生成する例です(以下の例では、 FE を生成後すぐに呼び出しを行っています)。

var foo = {};
 
(function initialize() {
 
  var x = 10;
 
  foo.bar = function () {
    alert(x);
  };
 
})();
 
foo.bar(); // 10;
 
alert(x); // "x" is not defined

関数 foo.bar は(その [[Scope]] プロパティを通じて)、関数 initialize の内部変数 x にアクセスできます。しかし同時に、 x はその外側からは直接にアクセスすることはできません。この手法は、多くのライブラリで「プライベート」データを作り、補助的な要素を隠蔽するために用いられています。このパターンではしばしば、 FE を初期化する名前は省略されます。

(function () {
 
  // スコープを初期化
 
})();

これは、ランタイムに条件付きで生成され、 VO を汚染しない FE の例です。

var foo = 10;
 
var bar = (foo % 2 == 0
  ? function () { alert(0); }
  : function () { alert(1); }
);
 
bar(); // 0
「取り囲む括弧」に関する問題

章の先頭に戻って質問にお答えしましょう。「定義してすぐに呼び出そうとする時、どうして関数を括弧で囲わなければならないのか?」。その答えは、式文の制約邦訳)のためです。

仕様によれは、式文( ExpressionStatement )は、波括弧開き( { )で始めることはできません。なぜなら、これを許可してしまうと、ブロックと区別が付かなくなるためです。そしてまた、式文はキーワード function で始めることも許されていません。これはもちろん、関数定義と区別できなくなるためです。というわけで、すぐに呼び出される関数を次のように(キーワード function で始めて)定義しようとすると、

function () {
  ...
}();
 
// あるいは名前付きで
 
function foo() {
  ...
}();

私たちは関数定義を扱っているのですが、この両方の場合で、パーサはパースエラーを出すことになります。しかし、そのパースエラーの理由にはバリエーションがあります。

このような定義をグローバルコードに配置した場合(つまり Program レベルに)、パーサはその関数を定義として扱わなければなりません。なぜならキーワード function で始まっているからです。そこで最初の例では、関数の名前が無いために SyntaxError となります(既述の通り、関数定義には常に名前がなければなりません)。

二つ目の例では名前 fooあり、関数定義は正しく生成されるはずです。しかし、また別のシンタックスエラーによってそうはなりません。そこに、式を含まないグループ演算子があるためです。正確には、この場合グループ演算子が関数定義に続いているのであって、関数呼び出しの括弧ではないのです!。従って、もし次のようなコードがあったとしたら、

// "foo" は関数定義です。
// そして、コンテキスト進入時に生成されます。
 
alert(foo); // function
 
function foo(x) {
  alert(x);
}(1); // そしてこれはただのグループ演算子で、呼び出しではありません!
 
foo(10); // これは呼び出しです。 10 です。

関数定義と式( 1 )を含んだグループ演算子という二つの構文的生成規則によって、全く問題はありません。上記の例は次と同様です。

// 関数定義
function foo(x) {
  alert(x);
}
 
// 式を含んだグループ演算子
(1);
 
// 別の関数式を含んだ、別のグループ演算子
(function () {});
 
// これもまた式を含んだ例
("foo");
 
// などなど

の中にこのような定義を取るとき、前述したように、曖昧さを理由にシンタックスエラーとなるでしょう。

if (true) function foo() {alert(1)}

仕様によれば、このような構造は構文的に誤りです(式文はキーワード function で開始できません)。しかし、後に見るように、どの実装もシンタックスエラーとなりません。ただし、それぞれ独自のやり方でこのケースを処理します。

こうしたことをふまえて、生成の直後に関数を呼び出ししたいということをパーサに伝えるには、どのようにすべきでしょうか?。答えは明白です。関数定義ではなく関数式であるべきです。そして式を生成する最も簡単は方法は、既にに触れたグループ演算子邦訳)を用いることです。グループ演算子の中にあるのは、常にです。従って、パーサはコードを関数式( FE )として認識し、曖昧さは無くなります。このような関数は実行時に生成され、(直後に)実行され、そして削除されます(もしこれに対する参照がなければ)。

(function foo(x) {
  alert(x);
})(1); // 問題なし。これはグループ演算子ではなく、呼び出しです。

この例では、末端の括弧( Arguments 生成規則)は関数の呼び出しであって、 FE の場合に見たグループ演算子ではありません。

気に留めておいていだたきたいのは、関数の定義直後の呼び出しの下記の例では、取り囲む括弧は必要ないということです。なぜなら関数は既に式を取る位置にいるからであり、パーサにもこれが、コード実行時に生成する FE として扱うものだと分かるからです。

var foo = {
 
  bar: function (x) {
    return x % 2 != 0 ? 'yes' : 'no';
  }(1)
 
};
 
alert(foo.bar); // 'yes'

一見すると関数に見える foo.bar ですが、実際には文字列です。ここでの関数は単純に条件となる引数によってプロパティを初期化するために用いられており、生成され、その直後に呼び出されています。

ここまでの通り、「括弧について」の質問に対する完全な回答は次のようになります。

グループ化の括弧は関数が式の位置におらず、生成の直後に呼び出したい場合に必要となる。この場合、行っていることは手動による関数の FE 化の指定である。

パーサが既に FE を扱うことを知っている場合、つまり関数が既に式の位置にいる場合は、括弧は必要ない

取り囲む括弧以外にも、関数を FE 化するために使うことのできる方法は他にもあります。例えば、

1, function () {
  alert('anonymous function is called');
}();
 
// あるいはこのように
!function () {
  alert('ECMAScript');
}();
 
// また、その他の手動の変換
 
...

しかし、現在はグループ化の括弧が最も広く使われており、的確な方法であると思います。

ちなみに、グループ演算子は括弧の無い関数の記述だけを囲うこともできますし、呼び出し括弧を含んで取り囲むこともできます。以下のどちらも正しい FE となります。

(function () {})();
(function () {}());
実装による拡張: 関数文

次の例は、仕様に従えばどんな実装系も処理しないはずのコードです。

if (true) {
 
  function foo() {
    alert(0);
  }
 
} else {
 
  function foo() {
    alert(1);
  }
 
}
 
foo(); // 1 あるいは 0 ? 異なる実装で試してみてください。

まず、規格によれば、この構文構造は一般的に誤りであるということをお伝えする必要があるでしょう。理由は、ご記憶の通り、関数定義( FD )はコードブロック中に現れることはできないためです( ifelse の中身はコードブロックです)。前述のように、 FD は次の二つの位置にのみ現れることができます。 Program レベルと、他の関数の本体の直下です。

上の例は、コードブロックは文のみを含むために、誤りとなります。ブロックの中で関数が現れることのできる位置は、このような文の中の一つ、式文です。しかし定義では、波括弧開きコードブロックと区別が付かなくなるため)及びキーワード function( FD と区別が付かなくなるため)で開始することはできません

しかしながら、規格ではエラー処理の章において、実装系がプログラムシンタックスを拡張して良いということになっています。そのような拡張の一つが、ブロックの中に現れる関数の場合に見られます。今日存在する全ての実装は、この場合例外を投げず、これを正常に処理します。ただし、それぞれ別々の方法で、ではありますが。

if-else 分岐が存在するということは、二つある関数のうちどちらかが定義されるという選択が行われることを示唆します。この決定はランタイムに行われますから、 FE を用いる必要が見えてきます。しかし、多くの実装は単に両方のための関数定義( FD )をコンテキスト進入時に生成し、さらにこの二つの関数は同じ名前を使っていますから、最後に宣言された関数のみが呼び出されるようになります。この例では、 else の分岐が決して実行されなくても、関数 foo1 を示します。

ところが、 SpiderMonkey 実装はこのケースを二つの方法を使って計らいます。まず一方ではこのような関数を定義として扱うことをせず(関数はコード実行時に条件を元に生成され)、しかしまた一方では本当の関数式としても取り扱いません。そもそも取り囲む括弧無しでは呼び出せないですし(もちろんパースエラー、「 FD と区別が付かないから」です)、さらに VO に保管されるのです。

私の考えでは、 SpiderMonkey は、関数の中間種( FE + FD )を用意し区別することで、このケースを上手に処理していると思います。こうした関数は条件を考慮しつつしかるべき時に生成され、しかしまた FE とは異なりむしろ FD のように、外側からも呼び出すことができます。この構文拡張は、 SpiderMonkey では関数文( Function Statement 略して FS )と呼んでおり、 MDN でも触れられている用語です。 JavaScript の開発者である Brendan Eich もまた、 SpiderMonkey 実装によるこの種の関数について触れています

名前付き関数式( NFE )の特徴

FE が名前を持つ場合(名前付き関数式( named function expression 略して NFE ))、ある一つの特徴が明らかになります。定義から分かるとおり(そしてこれまでの例で見てきたとおり)、関数式はコンテキストの変数オブジェクトに影響しません(これは定義の前後に名前によってそれを呼び出すことができないということです)。しかし FE は再帰的に名前によって自分自身を呼び出すことができます。

(function foo(bar) {
 
  if (bar) {
    return;
  }
 
  foo(true); // "foo" という名前は利用可能
 
})();
 
// しかし外側からは利用できない。これは正しい。
 
foo(); // "foo" is not defined

"foo" という名前はどこに保管されているのでしょうか?。 foo のアクティベーションオブジェクトでしょうか?。違います。誰も関数 foo の中で "foo" という名前を定義してはいません。 foo を生成した親コンテキストの変数オブジェクトでしょうか?。これもまた違います。定義を思い出して下さい。 FE は VO に影響しません。これは foo を外側から呼び出してみた時の結果に明らかです。それではどこで?

仕組みはこうです。コード実行時にインタプリタが名前付き FE に出会うと、インタプリタは補助的な特別オブジェクトを用意し、それをスコープチェーンの先頭に追加します。そして実際に FE が生成されるとき、 FE は [[Scope]] プロパティを受け取ります(『第4章 スコープチェーン』で既に見ました)。これは、関数を生成したコンテキストのスコープチェーンです(ですから、 [[Scope]] プロパティの中には特別なオブジェクトを含んでいます)。この後、 一意のプロパティとして、FE の名前が特別なオブジェクトに追加されます。このプロパティの値は FE への参照です。そうして、最後にスコープチェーンからこの特別なオブジェクトを削除します。このアルゴリズムを、擬似コードで見てみましょう。

specialObject = {};
 
Scope = specialObject + Scope;
 
foo = new FunctionExpression;
foo.Scope = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
 
delete Scope[0]; // specialObject をスコープチェーンの先頭から削除する。

従って、この関数の名前は外側からは利用できません(親スコープに存在しないからです)。しかし関数の [[Scope]] にはあらかじめ保存された特別なオブジェクトが用意されているため、関数の内部ではこの名前が利用できるのです。

ただし注意していただきたいのは、いくつかの実装、例えば Rhino では、この任意名を特別なオブジェクトではなく FE のアクティベーションオブジェクトに保存します。 Microsoft による実装、 JScript では、完全に FE の規則に反し、この名前を親変数オブジェクトに保管し、関数は(その任意名で)外側から利用可能になってしまっています。

NFE と SpiderMonkey

この問題を、さまざまな実装がどのように取り扱っているか見てみることにしましょう。 SpiderMonkey のいくつかのバージョンでは、バグとして(とはいえ、全て規格に従って実装されており、仕様の欠陥というべきものですが)考えられ得る、特別なオブジェクトに関する一つの特徴があり、それは識別子解決の仕組みに関係しています。スコープチェーン分析は二次元であり、識別子を解決する際スコープチェーン中の全てのオブジェクトについてプロトタイプチェーンもまた考慮に入れられる、という件です。

コードから、 Object.prototype にプロパティを定義し、そして「存在しない」変数を使えば、この仕組みを見て取ることができます。次の例では、名前 x を解決するために、 x が見つかることなくグローバルオブジェクトに探索が到達しています。しかし、 SpiderMonkey ではグローバルオブジェクトが Object.prototype を継承していますから、名前 xそこで解決するのです。

Object.prototype.x = 10;
 
(function () {
  alert(x); // 10
})();

アクティベーションオブジェクトはプロトタイプを持ちません。同様のふるまいは、内部関数を使った例で見ることができます。ローカル変数 x を定義し、内部関数( FD または匿名 FE )を宣言して、その内部関数から x を参照します。すると、この変数は Object.prototype ではなく、通常通り親関数コンテキスト(つまり、あるべき、そして実際にあるところ)にて解決されます。

Object.prototype.x = 10;
 
function foo() {
 
  var x = 20;
 
  // 関数定義
 
  function bar() {
    alert(x);
  }
 
  bar(); // 20, AO(foo) から
 
  // 匿名 FE でも同様
 
  (function () {
    alert(x); // 20, 再び AO(foo) から
  })();
 
}
 
foo();

その他の多くの実装と比較すれば例外的ではありますが、実装によっては、アクティベーションオブジェクトにプロトタイプをセットしているものがあります。 BlackBerry 実装においては、上記例における値 x は、 10 と解決されます。 Object.prototype 中に値が見つかってしまうため、 foo のアクティベーションオブジェクトに到達しないのです。

AO(bar FD または匿名 FE) -> 無し ->
AO(bar FD または億名 FE).Prototype -> 有り - 10

全く同じような現象を、今度は SpiderMonkey における名前付き FE の特別なオブジェクトに見ることができます。この特別なオブジェクト(標準によれば)が、普通のオブジェクト
なのです。「まるで new Object() 式で与えられる結果のよう」であり、従って Object.prototype を継承しているのです。これがまさに、 SpiderMonkey 実装(ただしバージョン1.7まで)に見ることのできる現象です。他の実装では( SpiderMonkey の新しいバージョンも含め)この特別なオブジェクトにプロトタイプをセットしてはいません。

function foo() {
 
  var x = 10;
 
  (function bar() {
 
    alert(x); // 20 、10 ではない。 AO(foo) に到達しないため。
 
    // "x" がスコープチェーン中で解決される順序は、
    // AO(bar) - 無し -> __specialObject(bar) -> 無し
    // __specialObject(bar).Prototype - 有り: 20
 
  })();
}
 
Object.prototype.x = 20;
 
foo();
NFE と JScript

Microsoft による ECMAScript 実装で、現在 Internet Explorer に積み込まれている実装である JScript ( JScript 5.8 - IE8 まで)には、名前付き関数式( NFE )に関して数多くのバグがあります。これらのバグは全て ECMA-262-3 標準に反しており、そのうちのいくつかは深刻なエラーに繋がる場合があります。

最初に、 JScript は、 FE を関数の名前によって変数オブジェクトに保管してはならないという規則を破っています。特別なオブジェクトに保管され、関数それ自身の内側からのみアクセスできる(それ以外のどこからもできない)はずの FE の任意名が、親変数オブジェクトに直接保管されてしまっているのです。しかも、 JScript においては
、名前付き FE は関数定義( FD )として扱われています。つまり、コンテキスト進入時に生成され、ソースコード中で定義されるより前に利用可能なのです。

// FE が、まるで FD のように、
// 定義より前の段階で、任意名をもって
// 変数オブジェクト中に利用可能。
testNFE();
 
(function testNFE() {
  alert('testNFE');
});

// さらにまた FD のように定義の後にも、
// 変数オブジェクト中の任意名によって
// アクセス可能。
testNFE();

ご覧の通り、これはまったくの規約違反です。

二番目は、名前付き FE を宣言時に変数に代入する場合です。 JScript は、二つの異なる関数オブジェクトを生成してしまいます。特に NFE の外側では本来その名前でアクセスできないはずということを考えるに、この振る舞いに理論的な名前を与えることは、難しいことです。

var foo = function bar() {
  alert('foo');
};
 
alert(typeof bar); // "function"、ここでも NFE が VO 中に存在します。ここで既に誤りです。
 
// しかし、より興味深いことに...
alert(foo === bar); // false!
 
foo.x = 10;
alert(bar.x); // undefined
 
// そうでありながら、
// 二つの関数は同じアクションをする。
 
foo(); // "foo"
bar(); // "foo"

これもまたご覧の通り、全くの混乱状態です。

しかしまたこれもお伝えしておきますが、 NFE を変数への代入から分けて記述し(たとえばグループ演算子などを使って)、その後で変数に代入した場合、同一性のチェックはまるで一つのオブジェクトであるように、 true を返します。

(function bar() {});
 
var foo = bar;
 
alert(foo === bar); // true
 
foo.x = 10;
alert(bar.x); // 10

今度の場合は説明が付きます。実際には、上の例同様に二つのオブジェクトが生成されています。ですが、結果として、最終的に、一つだけ残るのです。もしここでも、 NFE が関数定義( FD )として扱われていると考えてみれば、コンテキスト進入時に FD bar が生成されています。その後コード実行時に、第二のオブジェクト、関数式( FE ) bar が生成されますが、どこにも保存されません。従って、 FE bar への参照はどこにも無くなりますので、これは削除されます。結果として、オブジェクトが一つだけ、 FD bar が残ります。そしてそこへの参照が、変数 foo に代入されるというわけです。

第三に、 arguments.calle を経由した関数への間接参照の場合です。この場合、参照先はアクティベートされた際に使われた名前の関数になります。

var foo = function bar() {
 
  alert([
    arguments.callee === foo,
    arguments.callee === bar
  ]);
 
};
 
foo(); // [true, false]
bar(); // [false, true]

第四に、 JScript は NFE を通常の FD として扱い、条件演算には影響されません。つまり、 FD のように、 NFE がコンテキスト進入時に生成され、コード中で最後に定義されたものが使われるのです。

var foo = function bar() {
  alert(1);
};
 
if (false) {
 
  foo = function bar() {
    alert(2);
  };
 
}
bar(); // 2
foo(); // 1

この振る舞いもまた、「理論的に」説明できます。コンテキスト進入時に、最後に見つかった bar という名前の FD (内容が alert(2) のもの)が生成されます。その後、コード実行時に新しい関数、 FE bar が生成され、これへの参照が変数 foo に代入されます。こうして(コード上でより後にある、条件 falseif ブロックには到達しませんので)、関数 foo のアクティベーション結果は alert(1) となるのです。理屈ははっきりとしています。はっきりとしていますが、 IE のバグであることを考えると、「理論的に」という言葉を括弧書きにせざるを得ません。こうした実装は明らかに誤っていますし、 JScript のバグに依存するものだからです。

そして JSciprt における五つ目の NFE のバグは、非制約の識別子に値を代入することで( var キーワード抜きで)、グローバルオブジェクトに間接的にプロパティが生成される仕組みに関するものです。ここでは NFE は FD として扱われていますし、従って変数オブジェクトに保管されますので、非制約の識別子に対する代入(変数に対してではなく、グローバルオブジェクトの一般のプロパティに対する)は、関数名がその非制約な変数と同名の場合、このプロパティがグローバルにならないのです。

(function () {
 
  // var が無く、ローカルコンテキストの変数でもないので、
  // グローバルオブジェクトのプロパティのはず
 
  foo = function foo() {};
 
})();

// しかし、匿名関数の外部からは、
// foo という名前は利用できない。

alert(typeof foo); // undefined

ここでもまた、「理論的には」はっきりしています。関数定義(として扱われてしまっている関数式) foo は、コンテキスト進入時に、匿名関数のローカルコンテキストのアクティベーションオブジェクトに積み込まれます。そしてその後のコード実行時において、 foo という名前が既に AO に存在してしまっているため、つまりローカル変数のように扱われるわけです。結果、そこでの代入演算は単に既に AO に存在するプロパティ foo を更新するだけです。 ECMA-262-3 の理論に従えば、グローバルオブジェクトの新しいプロパティを生成するはずですが、そうはなりません。

Function コンストラクタで生成された関数

独自な特徴を持つため、この種の関数オブジェクトを FD や FE と分けて検討することにしました。その主な特徴は、このような関数の [[Scope]] プロパティはグローバルオブジェクトのみを含むということです。

var x = 10;
 
function foo() {
 
  var x = 20;
  var y = 30;
 
  var bar = new Function('alert(x); alert(y);');
 
  bar(); // 10, "y" is not defined
 
}

関数 barScope が、コンテキスト foo の AO を含んでいないことが見て取れます。変数 "y" にはアクセスできず、変数 "x" はグローバルコンテキストから取られています。ちなみに注意していただきたいのは、 Function コンストラクタはキーワード new が有っても無くても使えます。この場合、これらの機能は同等です。

このような関数の別の特徴は、同等文法の生成規則邦訳)および結合オブジェクト邦訳)です。この仕組みは、最適化を目的として仕様によって提案されているものです(しかし、実装がそうした最適化を用いないことも可能です)。例えば、今ここにループ中にて関数が詰め込まれる100要素の配列があったとして、実装系はこの結合オブジェクトの仕組みを利用できます。結果として配列の全ての要素に対したった一つの関数オブジェクトが使われるのです。

var a = [];
 
for (var k = 0; k < 100; k++) {
  a[k] = function () {}; // おそらくは、結合オブジェクトが利用される。
}

ただし、 Function コンストラクタで生成された関数は、決して結合されません

var a = [];
 
for (var k = 0; k < 100; k++) {
  a[k] = Function(''); // 必ず 100 個の別々の関数
}

結合オブジェクトに関するそのほかの例です。

function foo() {
 
  function bar(z) {
    return z * z;
  }
 
  return bar;
}
 
var x = foo();
var y = foo();

ここでは、実装はオブジェクト x と y を結合する(そして一つのオブジェクトを利用する)ことが可能です。なぜなら、実質的に関数(その内部 Scope プロパティを含め)の区別が付かないためです。このようなことから、 Function コンストラクタで生成された関数は、常により多いメモリリソースを要求することになります。

関数生成のアルゴリズム

関数生成の仕組みを擬似コードで表したものが下記のコードです(結合オブジェクトのステップは省略しています)。 ECMAScript の関数オブジェクトについてより理解を深めることができると思います。この機序は、関数の種類全てに同一です。

F = new NativeObject();
 
// プロパティ Class は "Function"
F.Class = "Function"
 
// 関数オブジェクトのプロトタイプ
F.Prototype = Function.prototype
 
// 関数そのものへの参照
// Call は呼び出し式である F() でアクティベートされる
// そして新しい実行コンテキストを生成する
F.Call = <reference to function>

// オブジェクトの一般的な組み込みコンストラクタ
// Construct はキーワード "new" でアクティベートされ
// そして新しいオブジェクトのためのメモリを確保する
// それから改めて F.Call を呼び出し、
// その際新たに生成されたオブジェクトを、
// "this" 値として渡す
F.Construct = internalConstructor
 
// 現在のコンテキストのスコープチェーン
// すなわち関数 F を生成したコンテキスト
F.Scope = activeContext.Scope
// もしこの関数が new Function(...) で
// 生成されたなら
F.Scope = globalContext.Scope
 
// 仮引数の数
F.length = countParameters
 
// F によって生成されるオブジェクトのプロトタイプ
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum} 、ループ中で対象にならない
F.prototype = __objectPrototype
 
return F

注意すべきところは、 F.Prototype関数(コンストラクタ) のプロトタイプであり、 F.prototypeこの関数で生成されるオブジェクトのプロトタイプであるということです(時折この用語には混乱が見られます。記事によっては F.prototype を「コンストラクタのプロトタイプ」としているものがありますが、それは誤りです)。

結論

この章はかなり長くなってしまいましたが、以降の章、オブジェクトやプロトタイプについて検討する章で、今回の内容である関数を、今度はコンストラクタとして見ていくことになるでしょう。いつものように、コメントでみなさんの質問に喜んでお答えします(訳注:例によって後日訳者の個人ブログにも訳文をアップいたしますので...、といいながら前章も移せていないですが、近日中に移します。その際はぜひご不明な点ご質問ください)。

参考文献

英語版翻訳: Dmitry A. Soshnikov[英語版].

英語版公開日時: 2010-04-05

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

オリジナルロシア語版公開日時: 2009-07-08

本シリーズはすべて英語版からの訳出です。



詳細 ECMA-262-3 第4章 スコープチェーン

どうもおつかれさまでございます。大形尚弘でございます。先日、 tumblr developer meetup 2011 に参加させていただき、「リブログのこの快感をどのようにサービスに取り入れるか」についてご参加の皆様と熱い議論を交わしたのですが、20時を過ぎた辺りで全員「そろそろ早く帰ってリブログしたいな...」という気持ちになりましたためスッと解散になったのが印象的でした。

ちなみに、「たんぽぽグループ」は実在いたします。みなさんの心の中に...。

というわけで、 Dmitry 先生の ECMA-262-3 シリーズもいよいよ中盤、スコープチェーンについてご一緒に勉強させてください。

詳細 ECMA-262-3 第4章 スコープチェーン

目次

  1. はじめに
  2. 定義
  3. 関数のライフサイクル
    1. 関数の生成
    2. 関数のアクティベーション
  4. スコープの特徴
    1. クロージャ
    2. 関数コンストラクタから生成された関数の [[Scope]] プロパティ
    3. 二次元のスコープチェーン探索
    4. グローバル及び eval コンテキストのスコープチェーン
    5. コード実行中にスコープチェーンを変更する
  5. 結論
  6. 参考文献

はじめに

変数オブジェクトについての第2章で既に触れたように、実行コンテキストのデータ(変数、関数定義、関数の仮引数)は、変数オブジェクトのプロパティとして保管されます。

また、変数オブジェクトはコンテキスト進入時に生成及び初期値の代入が行われ、コード実行フェーズにて変更が発生します。

この章は、実行コンテキストに直接に関係するさらにもう一つのトピックに充てられます。今回は、スコープチェーンについて検討します。

定義

簡潔に、要点だけ説明するならば、スコープチェーンとは主に内部関数に関係するものです。

ご存じの通り、 ECMAScript は内部関数の生成を許可しており、それらの関数を親の関数から戻すことさえできます。

var x = 10;
 
function foo() {
 
  var y = 20;
 
  function bar() {
    alert(x + y);
  }
 
  return bar;
 
}
 
foo()(); // 30

そしてまた、すべてのコンテキストは各々の変数オブジェクトを持ちます。グローバルコンテキストにはグローバルオブジェクト自身が、関数コンテキストにはアクティベーションオブジェクトが対応して存在します。

つまりスコープチェーンとは、まさに内部関数にとってのすべての(親の)変数オブジェクトのリストに他なりません。このチェーンが、変数の探索に用いられるものです。すなわち上の例では、 "bar" コンテキストのスコープチェーンは、 AO(bar) 、 AO(foo) 、 VO(global) を含んだリストなのです。

しかしこれをもう少し詳しく見てみましょう。

始めにまず定義を与え、その後に例を元に検討します。

スコープチェーンとは、実行コンテキストに関係して、識別子解決時の変数探索に用いられる変数オブジェクトのチェーンです。

関数コンテキストのスコープチェーンは関数呼び出し時に生成され、アクティベーションオブジェクトと関数の内部 Scope プロパティで構成されます。関数の Scope プロパティについてはこの後に詳しく検討します。

実行コンテキストから図式的に見れば、

activeExecutionContext = {
    VO: {...}, // あるいは AO
    this: thisValue,
    Scope: [ // スコープチェーン
      // 識別子探索のための、
      // すべての変数オブジェクトのリスト
    ]
};

この時、 Scope の定義は次のようになります。

Scope = AO + Scope

ここで例として、 Scope 及び Scope を通常の ECMAScript 配列として表すことができます。

var Scope = [VO1, VO2, ..., VOn]; // スコープチェーン

この構造の別な見方としては、チェーン中のすべてのオブジェクトにおいて親スコープへの(親変数オブジェクトへの)参照を持った、階層的なオブジェクトのチェーンが考えられます。

この見方は、変数オブジェクトに関数第2章で検討した一部の実装における __parent__ の考え方に対応します。

var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->
// などなど

しかし、配列を用いてスコープチェーンを表現する方が簡便ですので、ここではこのアプローチを採ることにします。実装のレベルにおいては、 __parent__ 機能を伴った階層型チェーンのアプローチが採られ得るわけですが、一方仕様では、抽象的には「スコープチェーンはオブジェクトのリストである」と示しています。配列を用いた抽象的な表現は、リストというコンセプトを説明するために都合の良い方法です。

これから検討していく AO と Scope の組み合わせ、及び識別子解決のプロセスは、関数のライフサイクルに結びついています。

関数のライフサイクル

関数のライフサイクルは、生成、そしてアクティベーション(呼び出し)の2ステージに分かれます。詳しく見ていきましょう。

関数の生成

ご存じの通り、関数定義はコンテキスト進入時に変数オブジェクトまたはアクティベーションオブジェクト( VO/AO )に積み込まれます。グローバルコンテキスト上の変数と関数定義の例を挙げてみましょう(グローバルコンテキストではグローバルオブジェクトが変数オブジェクトでした。覚えていますよね?)。

var x = 10;
 
function foo() {
  var y = 20;
  alert(x + y);
}
 
foo(); // 30

関数がアクティベーションされる段階に至れば、正しい(そして期待された)結果、30が返ってきます。しかしここには一つある重要な特徴があります。

ここまで、私たちは現在のコンテキストの変数オブジェクトについてのみ話してきました。つまり変数 "y" は、関数 "foo" の中で定義されていることは見て取れます( "foo" コンテキストの AO 中に、ということです)。しかし変数 "x" は、コンテキスト "foo" では定義されておらず、従って "foo" の AO にも積み込まれていません。一見すると、関数 "foo" にとって、変数 "x" は全く存在しないのです。後に見るように、これは一見しただけの結果です。しかしともかく、 "foo" のアクティベーションオブジェクトには、ただ一つのプロパティ、 "y" のみしか含まれていないのです。

fooContext.AO = {
  y: undefined // コンテキスト進入時:undefined 、アクティベーション時:20
};

それでは、関数 "foo" はどのようにして変数 "x" にアクセスするのでしょうか?。関数は、より上のコンテキストの変数オブジェクトにアクセスすることができる、そう考えるのが理に適っています。これは事実上、その通りです。実際には、この仕組みは関数の内部 Scope プロパティとして実装されています。

Scope は、現在の関数コンテキストより上の、すべての変数オブジェクトの階層的なチェーンであり、関数の生成時に、関数に対して保存されます。

ここは重要なポイントです。 Scope は関数生成時に、静的に、常に、そして関数が破壊されるまでの間ずっと、保存されるのです。つまり、その関数が決して呼び出されなくともScope プロパティはその関数オブジェクトに必ず書き込まれ、保存されているのです。

もう一つ気に留めておくべきポイントは、 Scope (スコープチェーン)コンテキストのプロパティであるのと異なり、 Scope は関数のプロパティであるということです。上に挙げた例では、関数 "foo" の Scope は次のようになります。

foo.Scope = [
  globalContext.VO // === グローバルオブジェクト
];

こうした後に、ご存じ関数呼び出しによって関数コンテキストへの進入が始まり、アクティベーションオブジェクトが作られ、 this 値と Scope (スコープチェーン)が決定されます。それでは次に、この段階について検討しましょう。

関数のアクティベーション

定義の項で触れたとおり、コンテキストに進入し AO/VO が生成された後、コンテキストの Scope プロパティ(変数探索にとってのスコープチェーンに当たるもの)が下記の通り定義されます。

Scope = AO|VO + Scope

注目は、アクティベーションオブジェクトが Scope 配列の先頭であるということです。すなわち、スコープチェーンの先頭に追加されるのです。

Scope = [AO].concat(Scope);

この特性は識別子解決のプロセスを考える上でとても重要です。

識別子解決とは、その変数(あるいは関数定義)が、スコープチェーン中のどの変数オブジェクトに属するのかを、決定するプロセスです。

このアルゴリズムからの戻り値には、常に Reference 型の値が取られます。この値の base 要素は対応する変数オブジェクト(あるいは変数が見つからなければ null )、プロパティ名要素は探索された(解決された)識別子です。 Reference 型の詳細については、第3章 this で詳しく考察しています。

識別子解決のプロセスには、変数の名前に対応するプロパティの探索が含まれます。つまり、スコープチェーン中の変数オブジェクトに対し、最も深いコンテキストから、スコープチェーンの最上位まで、連続的に検査が行われると言うことです。

従って、探索プロセスにおいては、あるコンテキストのローカル変数が、親コンテキストの変数よりも高い優先度となります。もし二つの変数が同じ名前を持ち、しかし異なるコンテキストにあった場合、より深いコンテキストの変数が最初に発見されるのです。

前述の例をもう少し複雑にして、内部段を追加してみましょう。

var x = 10;
 
function foo() {
 
  var y = 20;
 
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
 
  bar();
}
 
foo(); // 60

ここでは、各々の変数/アクティベーションオブジェクト、関数の Scope プロパティ、コンテキストのスコープチェーンは、次の通りになります。

グローバルコンテキストの変数オブジェクトは、

globalContext.VO === Global = {
  x: 10
  foo: <関数への参照>
};

関数 "foo" の生成時、 "foo" の Scope プロパティは、

foo.Scope = [
  globalContext.VO
];

関数 "foo" のアクティベーション(コンテキスト進入)時、コンテキスト "foo" のアクティベーションオブジェクトは、

fooContext.AO = {
  y: 20,
  bar: <関数への参照>
};

そしてコンテキスト "foo" のスコープチェーンは、

fooContext.Scope = fooContext.AO + foo.Scope // つまり
 
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

内部関数 "bar" の生成時、その Scope プロパティは、

bar.Scope = [
  fooContext.AO,
  globalContext.VO
];

"bar" のアクティベーション時、コンテキスト "bar" のアクティベーションオブジェクトは、

barContext.AO = {
  z: 30
};

そしてコンテキスト "bar" のスコープチェーンは、

barContext.Scope = barContext.AO + bar.Scope // つまり
 
barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

名前 "x" 、 "y" 、 "z" の識別子解決プロセスはそれぞれ...、

- "x"
-- barContext.AO // 無し
-- fooContext.AO // 無し
-- globalContext.VO // 発見 - 10
- "y"
-- barContext.AO // 無し
-- fooContext.AO // 発見 - 20
- "z"
-- barContext.AO // 発見 - 30

スコープの特徴

それでは次に、スコープチェーン及び関数の Scope プロパティに関係した、いくつかの重要な特徴について検討していきましょう。

クロージャ

ECMAScript におけるクロージャは、関数の Scope プロパティに直接関係します。ご説明したとおり、 Scope は関数生成時に保存され、関数オブジェクトが破壊されるまで存在し続けます。実際のところ、 クロージャとは、全くもって関数コードと Scope プロパティの組み合わせのことなのです。だからこそ、 Scope は関数が生成されたレキシカル環境(親の変数オブジェクト)を含んでいます。その後の関数アクティベーション時において、より上位のコンテキストにおける変数はこのレキシカルな(生成時に静的に保存された)変数オブジェクトのチェーン中から探索されます。

例です。

var x = 10;
 
function foo() {
  alert(x);
}
 
(function () {
  var x = 20;
  foo(); // 10 、 20 ではなく
})();

変数 "x" が、関数 "foo" の Scope に見つかることが分かります。つまりは、変数探索において、関数呼び出し時に作成される動的なチェーンではなく(その場合変数 "x" は 20 と解決されたでしょうが)、関数生成時に定義されたレキシカルな(閉じ込められた)チェーンが用いられているのです。

クロージャの別の古典的な例を見てみましょう。

function foo() {
 
  var x = 10;
  var y = 20;
 
  return function () {
    alert([x, y]);
  };
 
}
 
var x = 30;
 
var bar = foo(); // 無名関数が戻されてきます
 
bar(); // [10, 20]

ここでも、識別子解決には関数生成時に定義されたレキシカルなスコープチェーンが用いられていることが分かります。変数 "x" は、 30 ではなく 10 と解決されます。さらにこの例は、関数(この場合関数 "foo" から戻された無名関数)の Scope が、関数が定義されたコンテキストが終了してしまった後も依然として存在し続けるということを、はっきりと示しています。

クロージャの理論と、その ECMAScript における実装については、第6章 クロージャを参照してください。

関数コンストラクタから生成された関数の [[Scope]] プロパティ

これまでの例を通じて、関数はその生成時に Scope プロパティを取り、このプロパティを経由して全ての親コンテキストの変数にアクセスする様子を見てきました。しかしながら、このルールには一つ重要な例外が存在します。関数が Function コンストラクタで生成された場合です。

var x = 10;
 
function foo() {
 
  var y = 20;
 
  function barFD() { // 関数定義
    alert(x);
    alert(y);
  }
 
  var barFE = function () { // 関数式
    alert(x);
    alert(y);
  };
 
  var barFn = Function('alert(x); alert(y);');
 
  barFD(); // 10, 20
  barFE(); // 10, 20
  barFn(); // 10, "y" は定義されていない
 
}
 
foo();

ご覧の通り、 Function コンストラクタによって生成された関数 "barFn" からは、変数 "y" はアクセスできません。しかし、だからといって関数 "barFn" が内部 Scope プロパティを持たないという訳ではありません(だとすれば変数 "x" にもアクセスできないはずです)。ここで重要なのは、 Function コンストラクタを通じて生成された関数の Scope プロパティには、常にグローバルオブジェクトのみが含まれているということです。例えばこう考えてみてください、なぜならこのような関数によって、グローバルを除いた上位コンテキストのクロージャを生成することは、不可能だからです(訳注:関数の生成時とアクティベーション時が、 Function コンストラクタを使用した関数定義の場合どのようになるか、検討してみてください)。

二次元のスコープチェーン探索

もう一つ、スコープチェーン中の探索で重要なポイントは、変数オブジェクトのプロトタイプ(もし存在すれば)もまた、考慮に入れられるということです。 ECMAScript のプロトタイプ的な性質から、もしプロパティがそのオブジェクト中に見つからなければ、探索はプロトタイプチェーンに進んでゆくからです。これはある種、チェーンの二次元探索であると言えます。 (1) まずスコープチェーン探索が一コマ進められ、 (2) 次にそのスコープチェーン中の一コマ毎に、プロトタイプチェーンのコマの中へと探索が進みます。 Object.prototype にプロパティを定義してみると、この動作を実際に観察できます。

function foo() {
  alert(x);
}
 
Object.prototype.x = 10;
 
foo(); // 10

ただし以下の例の通り、アクティベーションオブジェクトはプロトタイプを持ちません。

function foo() {
 
  var x = 20;
 
  function bar() {
    alert(x);
  }
 
  bar();
}
 
Object.prototype.x = 10;
 
foo(); // 20

もし関数コンテキスト "bar" のアクティベーションオブジェクトがプロトタイプを持っていたとしたら、プロパティ "x" は Object.prototype にて解決されていたはずです。なぜなら、 コンテキスト "bar" の AO においては直接に解決されていないからです。しかし前者の例では、識別子解決のスコープチェーン探索が、「 Object.prototype を継承するグローバルオブジェクト」に行き着くことで(すべての実装がそうではありませんが)、 "x" は 10 と解決されるのです。

同様の例は、名前付き関数式( Named Function Expressions : NFE と略します)に関して、 SpiderMonkey のいくつかのバージョンで見ることができます。関数式の任意名を保存する特別なオブジェクトがあり、それが Object.prototype を継承しているのです。また、 BlackBerry 実装のいくつかのバージョンにおいては、アクティベーションオブジェクトObject.prototype を継承してしまっています。しかしこれらの特徴について、より詳しくは第5章 関数にて検討することにしましょう。

グローバル及び eval コンテキストのスコープチェーン

これは驚くべきことではありませんが、お伝えしておかなければならないでしょう。グローバルコンテキストのスコープチェーンは、グローバルオブジェクトのみを含みます。 "eval" タイプのコードのコンテキストでは、呼び出し元コンテキストと同じスコープチェーンになります。

globalContext.Scope = [
  Global
];
 
evalContext.Scope === callingContext.Scope;

コード実行中にスコープチェーンを変更する

ECMAScript には、コード実行フェーズでランタイムにスコープチェーンを変更できる二つの文が存在します。 with 文と、 catch 節です。これらは両方とも、スコープチェーンの先頭に、これらの文の中に現れる識別子を探索するために必要なオブジェクトを追加します。つまり、これが起こる場合にはスコープチェーンは図式的には次の通りとなります。

Scope = withObject|catchObject + AO|VO + Scope

この例の with 文は、その引数となるオブジェクトをスコープチェーンの先頭に加えています(そうすることで、このオブジェクトのプロパティがプリフィックス無しでアクセスできているのです)。

var foo = {x: 10, y: 20};
 
with (foo) {
  alert(x); // 10
  alert(y); // 20
}

スコープチェーンの変更を表してみると、

Scope = foo + AO|VO + Scope

もう一度、 with 文によってスコープチェーンの先頭に追加されたオブジェクトの中で識別子が解決される例をご紹介します。

var x = 10, y = 10;
 
with ({x: 20}) {
 
  var x = 30, y = 30;
 
  alert(x); // 30
  alert(y); // 30
}
 
alert(x); // 10
alert(y); // 30

ここで何が起こったのでしょうか?。まず、コンテキスト進入フェーズにおいて、 "x" および "y" 識別子が変数オブジェクトに積み込まれました。さらにランタイムコード実行フェーズで、次のような変更が行われていきます。

  • x = 10, y = 10;
  • オブジェクト {x: 20} がスコープチェーンの先頭に追加される。
  • with の内部の var 文が現れるが、ここで新たに生成されるものはない。なぜなら全ての変数はコンテキスト進入ステージで既にパースされ、追加されているからである。
  • "x" 値の変更のみが行われる。 "x" は今まさに第2ステップにてスコープチェーンの先頭に追加されたオブジェクト中に解決される。 "x" の値は 20 だったが、ここで 30 となる。
  • さらに "y" 値の変更が行われる。これは上の変数オブジェクトに解決されるので、 10 だったものが、 30 となる。
  • さらに with 文終了後、特別なオブジェクトがスコープチェーンから削除される(変更された値 "x" 、 30 も、このオブジェクトと同時に削除される)。すなわち、スコープチェーンの構造は with 文による拡張が行われる以前の状態に回復される。
  • 最後の二つの alert に見るように、現在の変数オブジェクトにおける "x" の値は同じまま残り、 "y" は with 文中で変更された 30 となる。

さらに、 catch 節もまた、引数である例外にアクセスするために、中間スコープオブジェクトを生成します。このオブジェクトの唯一のプロパティは例外の引数としての名前で、スコープチェーンの先頭に配置されます。図式的には、

try {
  ...
} catch (ex) {
  alert(ex);
}

スコープチェーンの変更は、

var catchObject = {
  ex: <例外オブジェクト>
};
 
Scope = catchObject + AO|VO + Scope

catch 節の働きが終了した後、スコープチェーンは以前の状態に回復されます。

結論

この章まで、実行コンテキストにまつわる全ての一般的な概念を検討してきました。続いて、予定では関数オブジェクトの詳細な分析と、関数の種類(関数定義、関数式)、それにクロージャへと進みます。ともあれ、クロージャはこの章で検討した Scope プロパティに直接関係しています。しかし、詳しくは専用の章に任せるとしましょう。コメントでみなさんの質問にお答えできることを楽しみにしています(訳注:例によって後日訳者の個人ブログにも訳文をアップいたしますので、そちらで喜んで質問にお答えいたします)。

参考文献

英語版翻訳: Dmitry A. Soshnikov[英語版].

英語版公開日時: 2010-03-21

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

オリジナルロシア語版公開日時: 2009-07-01

本シリーズはすべて英語版からの訳出です。



詳細 ECMA-262-3 第3章 this

どうもおつかれさまでございます。たんぽぽグループの大形尚弘でございます。好きな言語は Dylan です。好きな声優は五十嵐裕美さんです。

さて、週刊のはずが月刊になってしまった、 Dmitry 先生の ECMA-262-3 シリーズの第3章をお送りします。文中、未だ訳出の終わっていないスコープチェーンや関数の章への参照がありますが、特にスコープチェーンにおいてこの時点である程度理解しておきたいとお感じになる方もいらっしゃるかと思います。その辺りは、以前私個人のブログで翻訳・公開させいただいたコア・JavaScript ( JavaScript. The Core. )でも簡単に触れられておりますので、適宜ご参照ください。

また、本章とは全然関係ないのですが、先日 JavaScript Advent Calendar 2011 (オレ標準コース)に参加させていただき、 ECMAScript におけるオブジェクト指向について、また次期バージョンの ES.next におけるオブジェクト指向周辺の新しいシンタックスについてご紹介させていただきましたので、ぜひご印刷の上、おトイレの時間つぶしなどにご利用いただければ幸いです。

それでは本章、 JavaScript / ECMAScript で最も面白いところの一つ、「 this 」についてご高覧ください。

詳細 ECMA-262-3 第3章 this

目次

  1. はじめに
  2. 定義
  3. グローバルコード上の this 値
  4. 関数コード上の this 値
    1. Reference 型
    2. 関数呼び出しと非 Reference 型
    3. Reference 型と null の this 値
    4. コンストラクタとして呼び出された関数での this 値
    5. 関数呼び出し時に手動で this を指定する
  5. 結論
  6. 参考文献

はじめに

この章では、実行コンテキストに直接に関係するもうひとつのテーマについて考察します。議題は this キーワードです。

ご承知の通りこのトピックはとても難しく、時に異なる実行コンテキストにおける this 値の決定について議論の的になるポイントです。

多くのプログラマにとって、プログラム言語における this キーワードとは、オブジェクト指向プログラミングに密接に結びつき、コンストラクタによって新たに生成されるオブジェクトまさにそれ自体を参照するものとして考えられてきました。 ECMAScript でもその考え方は実装されているものの、これから見ていくように、ここでは生成されるオブジェクトのみには限らないのです。

それでは、 ECMAScript の this 値について、詳しく見てゆきましょう。

定義

this 値は、実行コンテキストのプロパティです。

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

ここで VO とは、前の章でお話しした変数オブジェクトです。

this は、実行可能コードの種類に直接に関係するものです。その値はコンテキスト進入時に決定され、コードがコンテキスト中を実行している間は不変です。

各ケースについて、さらに詳しく見てゆきましょう。

グローバルコード上の this 値

ここでは話は単純です。グローバルコード上では、 this 値は常にグローバルオブジェクトそのものです。従って間接的な参照が可能です。

// グローバルオブジェクトへの
// 明示的なプロパティ定義
this.a = 10; // global.a = 10
alert(a); // 10

// 制約されていない識別子への代入による、
// 暗黙のプロパティ定義
b = 20;
alert(this.b); // 20

// 変数定義によるこれも暗黙的な例。
// グローバルコンテキストの変数オブジェクトは、
// グローバルオブジェクトそのものである。
var c = 30;
alert(this.c); // 30

関数コード上の this 値

関数コード内で this が用いられるとき、話はもっと興味深くなります。このケースは最も複雑で、さまざまな議論の的になってきました。

この種類のコードにおける、最初の(そしておそらくは、一番重要な) this 値の機能は、関数には静的に束縛されないということです。

前述したように、 this 値はコンテキスト進入時に決定されます。つまり、関数コードの場合においては、その値は都度完全に異なる値を取りうるのです。

しかしながら、コードの実行時には、 this 値は不変です。すなわち、新しい値を代入することはできません。なぜなら、 this は変数ではないからです。(対照的に、プログラミング言語 Python では明確に定義された self オブジェクトというものがあり、これはランタイムに変更可能です。)

var foo = {x: 10};
 
var bar = {
  x: 20,
  test: function () {
 
    alert(this === bar); // true
    alert(this.x); // 20
 
    this = foo; // エラー。 this 値を変更することはできない。
 
    alert(this.x); // 上のエラーが無かったとすれば、20ではなく、10になっただろう。
 
  }
};

// コンテキスト進入時に
// this 値は "bar" オブジェクトに決定される。
// 理由は後ほど詳しく説明する。
bar.test(); // true, 20
 
foo.test = bar.test;

// しかし、同じ関数を呼び出しているのだが、
// ここでは this 値は "foo" を参照する。
foo.test(); // false, 10

関数コードにおいて、 this 値が参照する先には一体何が影響しているのでしょうか?。これにはいくつかの要因があります。

まず、通常の関数呼び出しでは、 this はそのコンテキストのコードをアクティベートする呼び出し元によって与えられます。つまりそれは、関数を呼び出す親コンテキストです。 this 値は呼び出し式の形によって決定されます(言い換えれば、構文的に見て関数がどのように呼び出されたかという形式です)。

どんなコンテキストでも問題なく this 値を決定できるようになるためには、これは心に留めておくべきとても重要なポイントです。まさに、呼び出し式の形、すなわち関数を呼び出す方法こそが呼び出されたコンテキストにおける this 値を決定します。それ以外にはあり得ません。

( JavaScript に関するいくつかの記事や、時に書籍でさえ、「 this 値は関数がどのように定義されているかで決まる。もしグローバル関数であれば this 値はグローバルオブジェクトに、もし関数があるオブジェクトのメソッドであれば常にそのオブジェクトにセットされる」と主張していますが、これは完全に誤りです)。一歩踏み込んでみれば、一般的なグローバル関数でさえ、異なる形式の呼び出し式によってアクティベートされ得、異なる this 値を取り得るのです。

function foo() {
  alert(this);
}
 
foo(); // グローバル
 
alert(foo === foo.prototype.constructor); // true

// しかし、同じ関数であっても
// 異なる呼び出し式の形式によっては、
// this 値もまた異なる。
foo.prototype.constructor(); // foo.prototype

同様に、あるオブジェクトのメソッドとして定義された関数を、 this 値がそのオブジェクトにセットされない形で呼び出すことも可能です。

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};
 
foo.bar(); // foo, true
 
var exampleFunc = foo.bar;
 
alert(exampleFunc === foo.bar); // true

// もう一度、同じ関数であっても、
// 異なる呼び出し式の形によって、
// 異なる this 値を取ります。
exampleFunc(); // global, false

それでは、この呼び出し式の形式が、どのように this 値に影響するのでしょうか?。 this 値の決定について十分に理解するには、ある内部型、 Reference 型について詳しく考察する必要があります。

Reference 型

擬似コードを用いれば、 Reference 型の値は二つのプロパティを持ったオブジェクトとして表せます。 base(プロパティが属するオブジェクト)と、その base における propertyName です。

var valueOfReferenceType = {
  base: ,
  propertyName: <プロパティ名>
};

Reference 型の値が現れるのは、次の二つの場合です。

  1. 識別子を取り扱うとき
  2. あるいは、プロパティアクセサを取り扱うとき

識別子解決の手順については、第4章 スコープチェーンにて詳しく検討します。ここでは、このアルゴリズムからの戻り値は、必ず Reference 型の値( this 値の決定にとって重要な)になると理解しておいてください。

識別子とは、変数名、関数名、関数の仮引数名、グローバルオブジェクトの非制約プロパティの名前です。例えば、下記の識別子の値に対し...

var foo = 10;
function bar() {}

演算の中間結果では、対応する Reference 型の値は次の通りです。

var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
var barReference = {
  base: global,
  propertyName: 'bar'
};

Reference 型の値からオブジェクトの実際の値を取得するには GetValue というメソッドがあり、擬似コードでは次のように表せます。

function GetValue(value) {
 
  if (Type(value) != Reference) {
    return value;
  }
 
  var base = GetBase(value);
 
  if (base === null) {
    throw new ReferenceError;
  }
 
  return base.Get(GetPropertyName(value));
 
}

内部 [[Get]] メソッドは、プロトタイプチェーンから継承されるプロパティの解析も含め、オブジェクトのプロパティの実の値を取り出します。

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

みなさんご存じの通り、プロパティへのアクセス式(プロパティアクセサ)には二つのバリエーションがあります。 ドット記法(プロパティ名が正しい識別子であり、予め分かっている場合)と 括弧記法です。

foo.bar();
foo['bar']();

演算の途上の戻り値は、ここでも Reference 型の値を持ちます。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
 
GetValue(fooBarReference); // function object "bar"

さてそれでは、最も重要な意味において、関数コンテキストの this 値と Reference 型の値は、どのように関係するのでしょうか?。この点が、この章の主題です。関数コンテキストにおける this 値決定の原則は、次のようになります。

関数コンテキストにおける this 値は、呼び出し元によって与えられ、呼び出し式の形式(関数呼び出しが構文的にどのように書かれているかということ)によって決定される

呼び出し括弧 ( ... ) の左辺が Reference 型の値を取る場合は、 this 値は Reference 型の値の base オブジェクトにセットされる。

その他すべての場合においては(すなわち Reference 型と区別されるその他すべての値を取る場合)、 this 値は常に null となる。しかし、 this 値が null となることは意味を成さないため、暗黙にグローバルオブジェクトに変換される。

例を見てみましょう。

function foo() {
  return this;
}
 
foo(); // グローバルオブジェクト

呼び出し括弧の左辺には Reference 型の値が取られていることが分かります(なぜなら foo は識別子だからです)。

var fooReference = {
  base: global,
  propertyName: 'foo'
};

このようにして、 this 値は、この Reference 型の値の base オブジェクトにセットされます。この場合、グローバルオブジェクトです。

同様にプロパティアクセサの場合も...

var foo = {
  bar: function () {
    return this;
  }
};
 
foo.bar(); // foo

ここでも、 base が foo オブジェクトである Reference 型の値を取りますので、 bar 関数のアクティベーション時の this 値には foo オブジェクトがセットされます。

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

しかし、全く同じ関数違う形式の呼び出し式でアクティベートすると、異なる this 値を取ることになります。

var test = foo.bar;
test(); // グローバルオブジェクト

なぜならば、識別子である test が、異なる Reference 型の値を取るためです。ここではグローバルオブジェクトたる base が、 this 値として用いられます。

var testReference = {
  base: global,
  propertyName: 'test'
};
補足: ES5 の strict mode(未訳) では、 this 値はグローバルオブジェクトに強制変更されません。その代わり、 undefined がセットされます。

このようにして、なぜ異なる形式の呼び出し式でアクティベートされた同じ関数が異なる this 値を取るのか、という理由を正確に説明することができます。中間計算値である Reference 型の値が、異なる値を取るため、ということがその答えです。

function foo() {
  alert(this);
}
 
foo(); // グローバルオブジェクト。なぜなら、
 
var fooReference = {
  base: global,
  propertyName: 'foo'
};
 
alert(foo === foo.prototype.constructor); // true
 
// 異なる形式の呼び出し式
 
foo.prototype.constructor(); // foo.prototype 。なぜなら、
 
var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

もう一つ、呼び出し式の形による this 値の動的な決定の、別の古典的な例を挙げておきます。

function foo() {
  alert(this.bar);
}
 
var x = {bar: 10};
var y = {bar: 20};
 
x.test = foo;
y.test = foo;
 
x.test(); // 10
y.test(); // 20

関数呼び出しと非 Reference 型

ところで、既に述べたように、呼び出し括弧の左辺が Reference 型ではなくその他すべての 型の値を取る場合には、 this 値は自動的に null にセットされ、結果として、グローバルオブジェクトになります。

そうした式の例を見てみましょう。

(function () {
  alert(this); // null => グローバルオブジェクト
})();

この場合、呼び出し括弧の左辺は関数オブジェクトであり、 Reference 型のオブジェクトではありません(識別子でも無ければ、プロパティアクセサでもありません)。従って、結果として this 値はグローバルオブジェクトにセットされます。

もう少し複雑な例を見てみましょう。

var foo = {
  bar: function () {
    alert(this);
  }
};
 
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
 
(foo.bar = foo.bar)(); // グローバル?
(false || foo.bar)(); // グローバル?
(foo.bar, foo.bar)(); // グローバル?

さて、中間計算値が Reference 型の値になるべきプロパティアクセサでありながら、なぜ、ある呼び出しにおいては base オブジェクト(すなわち foo )ではなくグローバルオブジェクトthis 値に取るのでしょうか?

問題となるのは最後の三つです。ある演算を行った後、呼び出し括弧の左辺の値が、 Reference 型の値ではなくなっているのです。

一番最初の例では、すべては明白です。そこには間違いなく Reference 型が存在し、その結果として、 this 値はその base オブジェクト、つまり foo になります。

二番目の例は、グループ演算子です。しかしこのグループ演算子は、既に見たような Reference 型の値からオブジェクトの実値を取得するメソッド、すなわち GetValue呼び出しません(仕様 11.1.6の注釈(邦訳)を参照してください)。従って、グループ演算子の評価の戻り値は、依然として Reference 型の値であるため、 this 値は再び base オブジェクトである、 foo にセットされます。

三番目のケースは、代入演算子です。代入演算子はグループ演算子と異なり、 GetValue メソッドを呼び出します(仕様 11.13.1 のステップ3(邦訳)を参照してください)。結果として、この代入演算子の戻り値は Reference 型の値ではなく関数オブジェクトになるため、 this 値は null にセットされ、結果として、グローバルオブジェクトになるのです。

四番目、五番目のケースも同様です。カンマ演算子論理和式GetValue メソッドを呼び出します。従って、 Reference 型の値を失い、関数 型の値を得ます。そして再び、 this 値はグローバルオブジェクトになります。

Reference 型と null の this 値

ある呼び出し式が、呼び出し括弧の左辺を Reference 型の値と決定しながら、 this 値が null 、結果としてグローバルオブジェクトになる場合があります。これは、 Reference 型の値の base オブジェクトが、アクティベーションオブジェクトである場合に関係しています。

こうした場面は、内部関数が親から呼び出された場合の例に見ることができます。第2章で学んだように、ローカル変数、内部関数、及び仮引数は与えられた関数のアクティベーションオブジェクトに保管されます。

function foo() {
  function bar() {
    alert(this); // グローバルオブジェクト
  }
  bar(); // AO.bar() に等しい
}

アクティベーションオブジェクトは、 this 値として常に null を返します(擬似コード AO.bar() は、 null.bar() に等しくなります)。ここでまた再び前述した仕組みに基づいて、 this 値はグローバルオブジェクトにセットされます。

with オブジェクトが関数名のプロパティを持つとき、 with 文のブロック内部で関数が呼び出された場合に、例外があり得ます。 with 文は、独自の with オブジェクトをスコープチェーンの先頭に付け加えます。つまり、アクティベーションオブジェクトのにです。よって、(識別子またはプロパティアクセサに) Reference 型の値を持ちますので、 base オブジェクトはアクティベーションオブジェクトではなく、 with 文のオブジェクトになるのです。ちなみに、この件は内部関数だけでなく、グローバル関数にも影響する問題です。なぜなら with オブジェクトは、スコープチェーン中のより高位なオブジェクト(グローバルオブジェクトやアクティベーションオブジェクト)を隠してしまうからです。

var x = 10;
 
with ({
 
  foo: function () {
    alert(this.x);
  },
  x: 20
 
}) {
 
  foo(); // 20
 
}
 
// なぜなら
 
var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

同様の場面は、 catch 節の実引数である関数を呼び出す際にも起こりえます。この場合は、 catch オブジェクトがスコープチェーンの先頭に追加されます。つまり with の場合と同様に、アクティベーションオブジェクトやグローバルオブジェクトの手前に、です。しかし、この挙動は ECMA-262-3 のバグであると認識されており、標準の新しいバージョン ECMA-262-5 では、このような関数のアクティベーションでは、 this 値は catch オブジェクトではなく、グローバルオブジェクトにセットされます。

try {
  throw function () {
    alert(this);
  };
} catch (e) {
  e(); // __catchObject / ES3 、 (修正されて正しくは)グローバルオブジェクト / ES5
}
 
// 考え方としては
 
var eReference = {
  base: __catchObject,
  propertyName: 'e'
};
 
// しかしこれはバグであるため、
// this 値は global に強制される。
// null => global
 
var eReference = {
  base: global,
  propertyName: 'e'
};

さらに、名前付き関数式(関数についての詳細は第5章 関数を参照ください)を再帰的に呼び出す際も同様です。関数の最初の呼び出し時には、 base オブジェクトは親アクティベーションオブジェクト(またはグローバルオブジェクト)になるわけですが、再帰呼び出し時には、 base オブジェクトが関数式の任意な名前を保管する、特別なオブジェクトとなるのです。ただし、このケースでは、 this 値は常にグローバルオブジェクトにセットされるようになっています。

(function foo(bar) {
 
  alert(this);
 
  !bar && foo(1); // (再帰呼び出し)特別なオブジェクトである"べき"だが、常にグローバルオブジェクトとなる
 
})(); // (1回目:通常の呼び出し)グローバルオブジェクト

コンストラクタとして呼び出された関数での this 値

関数コンテキストの this 値にまつわるケースがもう一つあります。関数をコンストラクタとして呼び出した場合です。

function A() {
  alert(this); // 下で新しく作られるオブジェクト - "a" オブジェクト
  this.x = 10;
}
 
var a = new A();
alert(a.x); // 10

この場合、 new 演算子邦訳)が関数 A の内部 [[Construct]]邦訳)メソッドを呼び出し、続いて、オブジェクト生成後に、まったく同じ関数 A の内部 [[Call]]邦訳)メソッドを呼び出し、 this 値として新しく生成されたオブジェクトを渡します。

関数呼び出し時に手動で this を指定する

Function.prototype (すなわちすべての関数からアクセス可能)には二つのメソッドがあり、これらを用いれば、関数呼び出し時の this 値を手動で指定することができます。 .apply.call メソッドです。

これらは共に、呼び出されるコンテキストで使われる this 値を第一引数として受け取ります。差異はあまり重要ではありません。 .apply は必ず配列(または配列様オブジェクト、例えば arguments 等)を第二引数として受け取り、 .call はどんな引数でも受け取ります。この二つのメソッドに共通して必須なのは、第一引数、 this値です。

例です。

var b = 10;
 
function a(c) {
  alert(this.b);
  alert(c);
}
 
a(20); // this === グローバルオブジェクト, this.b == 10, c == 20
 
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

結論

この章では、 ECMAScript における this キーワードの機能について考察しました(そしてそれらは、 C++ や Java のものとは対照的なものでした)。この章が、 ECMAScript における this キーワードの働きをより正確に理解する手助けになることを期待します。そしていつものように、みなさんの質問にはコメントにて喜んでお答えします(訳注:このブログではコメントをいただけないのですが、日本語で拙ブログにコメントいただければ、わかる限りで訳者も喜んでお答えします)。

参考文献

英語版翻訳: Dmitry A. Soshnikov 、 Stoyan Stefanov の助力を借りて [英語版].

英語版公開日時: 2010-03-07

オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]

補足と訂正: Zeroglif

オリジナルロシア語版公開日時: 2009-06-28; 更新:2010-03-07

本シリーズはすべて英語版からの訳出です。