読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

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

ECMA-262-3 ECMAScript JavaScript 翻訳

あけましておめでとうございます。おつかれさまでございます。先日渋谷の、会社から徒歩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

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