ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript クロージャ パート 2: クロージャの実装
理論的な部分について説明した後、ECMAScript でクロージャがどのように実装されるかを紹介します。ここでもう一度強調しておく価値があります。ECMAScript は静的 (字句) スコープのみを使用します (一方、Perl などの言語は変数宣言に静的スコープと動的スコープの両方を使用できます)。
var x = 10; function foo() { alert(x); } (function (funArg) { var x = 20; // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了 funArg(); // 10, 而不是20 })(foo);
技術的に言えば、関数を作成した親コンテキストのデータは、関数の内部プロパティ [[Scope]] に格納されます。 [[Scope]] が何なのかわからない場合は、まず前の章を読むことをお勧めします。この章では、[[Scope]] について詳しく説明しています。 [[Scope]] とスコープ チェーンの知識を完全に理解していれば、クロージャも完全に理解できるようになります。
関数作成アルゴリズムによると、ECMAScript では、すべての関数がクロージャであることがわかります。これは、関数が作成されたときに上位コンテキストのスコープ チェーンを保存するためです (例外を除く) (この関数が後でアクティブ化されるかどうかに関係なく) - [[スコープ]] は関数の作成時に利用可能です):
var x = 10; function foo() { alert(x); } // foo是闭包 foo: <FunctionObject> = { [[Call]]: <code block of foo>, [[Scope]]: [ global: { x: 10 } ], ... // 其它属性 };
前述したように、最適化の目的で、関数が自由変数を使用しない場合、実装は内部の副作用ドメイン チェーンに保存されない場合があります。ただし、ECMA-262-3 仕様には何も記載されていません。したがって、通常、すべてのパラメータは作成フェーズ中に [[Scope]] 属性に保存されます。
一部の実装では、クロージャ スコープへの直接アクセスが可能です。たとえば、Rhino では、関数の [[Scope]] 属性には、非標準の __parent__ 属性があります:
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 では、クロージャが作成されます同じ親コンテキスト内で [[Scope]] 属性を共有します。言い換えれば、クロージャによる [[Scope]] 内の変数の変更は、他のクロージャによるその変数の読み取りに影響します:
var firstClosure; var secondClosure; function foo() { var x = 1; firstClosure = function () { return ++x; }; secondClosure = function () { return --x; }; x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中 alert(firstClosure()); // 3, 通过第一个闭包的[[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」はパラメーター「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]] 属性が実際に必要な値を持っていることがわかります。この目的のために、[[Scope]] に追加の変数オブジェクトを作成する必要があります。返された関数で「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
別の機能はクロージャから返されます。 ECMAScript では、クロージャ内の return ステートメントは、制御フローを呼び出しコンテキスト (呼び出し元) に返します。 Ruby などの他の言語では、多くの形式のクロージャがあり、対応するクロージャの戻り値も異なります。次の方法が可能です。呼び出し元に直接返されることもあれば、場合によってはコンテキストから直接終了することもあります。 。
ECMAScript の標準の終了動作は次のとおりです:
function getElement() { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // 返回给函数"forEach"函数 // 而不是返回给getElement函数 alert('found: ' + element); // found: 2 return element; } }); return null; }
ただし、ECMAScript では、try catch によって次の効果を実現できます:
var $break = {}; function getElement() { try { [1, 2, 3].forEach(function (element) { if (element % 2 == 0) { // // 从getElement中"返回" alert('found: ' + element); // found: 2 $break.data = element; throw $break; } }); } catch (e) { if (e == $break) { return $break.data; } } return null; } alert(getElement()); // 2
理論バージョン
ここで説明すると、開発者はしばしば誤ってクロージャを単純化し、親からクロージャを理解します。 context 内部関数を返すと、匿名関数のみがクロージャになり得ることも理解されます。
繰り返しますが、スコープ チェーンのため、すべての関数はクロージャです (関数の種類に関係なく、匿名関数、FE、NFE、FD はすべてクロージャです)。
関数の [[Scope]] にはグローバル オブジェクトのみが含まれるため、関数のタイプは 1 つだけです。それは、Function コンストラクターを通じて作成された関数です。この問題をより明確にするために、ECMAScript のクロージャーの 2 つの正しいバージョン定義を示します。
ECMAScript では、クロージャーは以下を参照します。
理論的な観点から: すべての関数。それらはすべて、作成時に上位コンテキストのデータを保存するためです。これは、関数内のグローバル変数へのアクセスは自由変数へのアクセスと同等であるため、単純なグローバル変数にも当てはまります。このとき、最も外側のスコープが使用されます。
実用的な観点から: 以下の関数はクロージャとみなされます:
それが作成されたコンテキストが破棄されたとしても、それはまだ存在します (たとえば、内部関数は親関数から戻ります)
自由変数はコード内で参照されています
以上です JavaScript クロージャ パート 2: クロージャ実装の内容 詳細については、PHP 中国語 Web サイト (www.php.cn) に注目してください。