ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScriptを深く理解するシリーズ(16) Closures_javascriptスキル

JavaScriptを深く理解するシリーズ(16) Closures_javascriptスキル

WBOY
WBOYオリジナル
2016-05-16 17:54:241175ブラウズ

はじめに
この章では、JavaScript でよく議論されるトピックであるクロージャを紹介します。実際、閉店についてはすでに誰もが話し合っています。それにもかかわらず、ここでは理論的な観点からクロージャについて説明し、ECMAScript のクロージャが実際に内部でどのように動作するかを見ていきます。

前の記事で述べたように、これらの記事は一連の記事であり、相互に関連しています。したがって、この記事で紹介する内容をより深く理解するには、まず第 14 章のスコープチェーンと第 12 章の変数オブジェクトを読むことをお勧めします。

英語の原文: http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/
はじめに
ECMAScript クロージャーについて直接議論する前に、やはり関数型プログラミングについて学ぶ必要があります。いくつかの基本的な定義。

ご存知のとおり、関数型言語 (ECMAScript もこのスタイルをサポートしています) では、関数はデータです。たとえば、関数を変数に割り当てたり、パラメータとして他の関数に渡したり、関数から返したりすることができます。このような関数には特別な名前と構造があります。

定義
関数引数 (「Funarg」) — 値が関数である引数です。
関数引数 (「Funarg」) — 値が関数である引数です。
例:

コードをコピー コードは次のとおりです。

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

exampleFunc(function () {
alert('funArg');
});上記の例では、funarg の実パラメータは、実際には exampleFunc に渡される匿名関数です。
逆に、関数パラメータを受け取る関数は高階関数 (略して HOF) と呼ばれます。関数関数、部分数学理論、または演算子とも呼ばれます。上記の例では、exampleFunc がそのような関数です。

前述したように、関数はパラメータとしてだけでなく戻り値としても使用できます。このような関数を返す関数は、関数値を持つ関数または関数値関数と呼ばれます。



コードをコピー コードは次のとおりです。 (function functionValued() {
return function ( ) {
alert('返された関数が呼び出されます');
})()();


という形式の関数です。通常のデータ (例: パラメータが渡されるとき、関数パラメータを受け入れるとき、または関数値を返すとき) は、ファーストクラス関数 (一般的に言えば、ファーストクラス オブジェクト) と呼ばれます。 ECMAScript では、すべての関数はファーストクラスのオブジェクトです。

通常のデータとして存在できる関数 (たとえば、パラメーターが渡される場合、関数パラメーターを受け入れる場合、または関数値を返す場合) は、第一級関数 (一般的に言えば、第一級オブジェクト) と呼ばれます。

ECMAScript では、すべての関数はファーストクラスのオブジェクトです。

それ自体をパラメータとして受け入れる関数は、自動適用関数または自己適用関数と呼ばれます:




コードをコピー
コードは次のとおりです。 (function selfApplicative(funArg) {
if (funArg && funArg === selfApplicative) {
alert('self- applicative');
return;
}

})(); 戻り値として selfこの機能を自動レプリケーション機能または自己レプリケーション機能と呼びます。通常、「自己複製」という用語は文献で使用されます:




コードをコピー

コードは次のとおりです:

(function selfReplicative() { return selfReplicative; })(); 自己複製関数のより興味深いパターンの 1 つは、1 つだけを受け入れることです。コレクション自体を受け入れるのではなく、パラメータとしてコレクションの項目を受け入れます。


コードをコピー

コードは次のとおりです:

// コレクションを受け入れる関数function registerModes(modes) { modes.forEach(registerMode, modes); } // 使用法registerModes(['roster', 'accounts', 'groups'] );
// 自己コピー関数の宣言
function modes(mode) {
registerMode(mode); // モードを登録します
return modes; // 関数自体を返します。
}

// 使用法、モードチェーン呼び出し
modes('roster')('accounts')('groups')

// 多少似ています: jQueryObject.addClass ("a") .toggle().removClass("b")


しかし、コレクションを直接渡すのは比較的効果的で直感的です。

関数パラメータで定義された変数は、「funarg」がアクティブ化されているときにアクセスできます (コンテキスト データを格納する変数オブジェクトはコンテキストが入力されるたびに作成されるため):
コードをコピーします コードは次のとおりです:

function testFn(funArg) {
//funarg がアクティブ化されると、ローカル変数 localVar は次のようになります。
funArg(10); // 20
funArg(20) // 30

}

testFn(function (arg) {
var localVar = 10;
alert(arg localVar);
});

ただし、ECMAScript では関数を親関数にカプセル化でき、次の変数を使用できることがわかります。親関数のコンテキスト。この機能により、funarg の問題が発生する可能性があります。

Funarg 問題
スタック指向プログラミング言語では、関数がアクティブ化されるたびに、これらの変数と関数パラメーターがスタックにプッシュされます。

関数が戻ると、これらのパラメータはスタックから削除されます。このモデルでは、関数を関数値として (たとえば、親関数からの戻り値として) 使用することに重大な制限が設けられています。ほとんどの場合、関数に自由変数がある場合に問題が発生します。

自由変数とは、関数内で使用される変数を指しますが、関数パラメーターでも関数のローカル変数でもありません。

例:
コードをコピーします コードは次のとおりです。

function testFn() {

var localVar = 10; innerFn( innerParam) {
alert(innerParam localVar);

return innerFn;
var someFn = testFn(); ); // 30


上記の例では、innerFn 関数の場合、localVar は自由変数です。

スタック指向モデルを使用してローカル変数を保存するシステムの場合、これは、testFn 関数呼び出しが終了すると、そのローカル変数がスタックから削除されることを意味します。このように、外部から innerFn への関数呼び出しが行われた場合、エラーが発生します(localVar 変数が存在しなくなるため)。

また、上記の例では、スタック指向の実装モデルでは、戻り値として innerFn を返すことは単純に不可能です。これは testFn 関数のローカル変数でもあるため、testFn が戻ったときにも削除されます。

もう 1 つの問題は、システムが動的スコープを使用し、関数が関数パラメーターとして使用される場合です。

次の例 (疑似コード) を見てください:




コードをコピーします

コードは次のとおりです: var z = 10; function foo() { alert(z); // 10 – を使用します。静的スコープと動的スコープ When
(function () {

var z = 20;
foo(); // 10 – 静的スコープを使用、20 – 動的スコープを使用

})();

// foo をパラメータとした場合も同様
(function (funArg) {

var z = 30;
funArg () ; // 10 – 静的スコープ、30 – 動的スコープ

})(foo);


動的スコープを使用すると、システムが管理されることがわかります。変数の動的なスタックを通じて。したがって、自由変数は、関数の作成時に保存された静的スコープ チェーンではなく、現在アクティブな動的チェーンでクエリされます。

これにより競合が発生します。たとえば、Z がまだ存在するとしても (スタックから変数を削除する前述の例とは対照的に)、Z のどの値がさまざまな関数呼び出し (どのコンテキスト、どのスコープからのクエリ) を引き受けるのかという疑問が残ります。

上記では、関数が戻り値として返されるかどうか (最初のタイプの問題) と、関数が関数パラメーターとして使用されるかどうか (2 番目のタイプの問題) に応じて、2 つのタイプの funarg 問題について説明しています。 。

上記の問題を解決するために、クロージャの概念が導入されます。
クロージャ
クロージャは、コードのブロックと、コードのブロックが作成されたコンテキスト内のデータの組み合わせです。
次の例 (疑似コード) を見てみましょう:




コードをコピーします


コードは次のとおりです:

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

};
上記の例では、「fooClosure」の部分が擬似コードです。同様に、ECMAScript では、「foo」関数には、関数のコンテキストを作成するスコープ チェーンという内部プロパティがすでにあります。

「語彙」は通常省略されます。上の例は、クロージャの作成時にコンテキスト データが保存されることを強調しています。次回関数が呼び出されるとき、自由変数は保存された (クロージャ) コンテキストで見つかり、上記のコードに示されているように、変数 "z" の値は常に 10 になります。

定義では「コード ブロック」というより広い用語を使用しますが、通常 (ECMAScript では) よく使用する関数を使用します。もちろん、クロージャのすべての実装がクロージャと関数を結びつけるわけではありません。たとえば、Ruby 言語では、クロージャはプロシージャ オブジェクト、ラムダ式、またはコード ブロックである可能性があります。

コンテキストが破棄された後にローカル変数を保存するという目的では、スタックベースの実装は明らかに適用できません (スタックベースの構造と競合するため)。したがって、この場合、上位スコープのクロージャー データは、ガベージ コレクター (GC と呼ばれるガベージ コレクター) と参照カウント (参照カウント) の使用と組み合わせて、動的にメモリを割り当てること (「ヒープ」実装に基づいて) によって実装されます。 )。この実装はスタックベースの実装よりもパフォーマンスが劣りますが、どの実装でも常に最適化できます。関数が自由変数、関数パラメータ、または関数値を使用しているかどうかを分析し、状況に基づいて決定できます - はい データをスタックまたはヒープ内にあります。

ECMAScript クロージャの実装
理論的な部分を説明した後、ECMAScript でクロージャがどのように実装されるかを紹介します。ここでもう一度強調しておく価値があります。ECMAScript は静的 (字句) スコープのみを使用します (一方、Perl などの言語は変数宣言に静的スコープと動的スコープの両方を使用できます)。
コードをコピーします コードは次のとおりです。

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

(function (funArg) {

var x = 20;

//変数 "x "関数の作成時に保存される (字句) コンテキストに保存される静的
funArg() // 20 ではなく 10

技術的に言えば、関数を作成した親コンテキストのデータは、関数の内部プロパティ [[Scope]] に格納されます。 [[Scope]] が何なのかわからない場合は、まず第 14 章を読むことをお勧めします。この章では、[[Scope]] について詳しく説明しています。 [[Scope]] とスコープ チェーンの知識を完全に理解していれば、クロージャも完全に理解できるようになります。

関数作成アルゴリズムによると、ECMAScript ではすべての関数がクロージャであることがわかります。これは、関数が作成されたときに (例外を除いて) 上位コンテキストのスコープ チェーンを保存するためです (この関数が後でアクティブ化します - [[スコープ]] は関数の作成時に存在します):



コードをコピー
コードは次のとおりです: var x = 10; function foo() {
alert(x);

// foo はクロージャ
foo: = {
[[Call]]: ,
[[スコープ]]: [
global: {
x: 10
}
],
... // その他のプロパティ
};


前述したように、関数が使用されていない場合は、自由変数の場合、実装は副作用ドメイン チェーンに保存されない可能性があります。ただし、ECMA-262-3 仕様には何も記載されていません。したがって、通常、すべてのパラメータは作成フェーズ中に [[Scope]] 属性に保存されます。

一部の実装では、クロージャ スコープに直接アクセスできます。たとえば、Rhino には関数の [[Scope]] 属性に非標準の __parent__ 属性があり、これは第 12 章で紹介されました:



コピー コード

コードは次のとおりです: var global = this; var foo = (function () {
var y = 20;

関数 () {

}); foo(); // 20
alert(foo.__parent__.y) // 20

foo.__parent__.y = 30;
foo(); 🎜>// スコープチェーンを介して先頭に移動できます
alert(foo.__parent__.__parent__ === global) // true
alert(foo.__parent__.__parent__.x);


すべてのオブジェクトは [[Scope]] を参照します
ここにも注意してください: ECMAScript では、同じ親コンテキストで作成されたクロージャーは [[Scope]] 属性を共有します。言い換えれば、[[Scope]] の変数に対して特定のクロージャによって行われた変更は、他のクロージャによるその変数の読み取りに影響します:

つまり、すべての内部関数は同じ親スコープ
コードをコピーします コードは次のとおりです。 🎜>function foo() {
var x = 1;

firstClosure = function () { return x; }; x; };

x = 2; // 2 つのクロージャによって共有される [[Scope]] 内の AO["x"] に影響します。 、最初のクロージャの [[Scope]]
}

foo();

alert(firstClosure()) // 4
alter(secondClosure()); // 3


この関数に関して非常によくある誤解がありますが、ループ ステートメント内で関数を作成すると (内部的にカウントされる) 結果が得られないことがよくありますが、各関数には次のような結果が期待されます。独自の価値。




コードをコピーします

コードは次のとおりです。

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

上記の例は、同じコンテキストで作成されたクロージャが [[Scope]] 属性を共有することを証明しています。したがって、上位コンテキストの変数「k」は簡単に変更できます。




コードをコピー


コードは次のとおりです:


activeContext.Scope = [
。 .. // その他の変数オブジェクト
{data: [...], k: 3} // アクティブなオブジェクト
]
data[0].[[Scope]] == = スコープ; data[1].[[スコープ]] === スコープ; ちなみに、この機能を有効にすると、最後に使用されるkは3になりました。以下に示すように、クロージャーを作成すると、この問題を解決できます。


コードをコピー


コードは次のとおりです:


var data = [];
for (var k = 0; k data[k] = (function _helper(x) {
return function () {
alert(x); })(k); // 「k」値を渡します } // 結果は次のようになります。正しい data[0](); // 0 data[1]() // 1
data[2]();上記のコードで何が起こるかを見てみましょう。関数「_helper」を作成した後、パラメーター「k」を渡すことで関数がアクティブになります。その戻り値も関数であり、対応する配列要素に格納されます。この手法により次のような効果が得られます。関数がアクティブ化されるたびに、「_helper」はパラメータ「x」を含む新しい変数オブジェクトを作成します。「x」の値は渡された「k」の値になります。このようにして、返される関数の [[スコープ]] は次のようになります:




コードをコピー


コードは次のとおりです。


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}, _ヘルパー コンテキスト内のアクティブ オブジェクト AO: {x: 2}
];
現時点では、関数の [[Scope]] 属性に実際に必要な値があることがわかります。この目的を達成するには、[[Scope]] に追加の変数オブジェクトを作成する必要があります。返された関数で「k」の値を取得したい場合、値は 3 のままであることに注意してください。

ところで、JavaScript を紹介する記事の多くは、追加で作成した関数だけがクロージャであると信じていますが、これは間違いです。実際には、この方法が最も効果的ですが、理論的な観点から見ると、ECMAScript の関数はすべてクロージャです。

ただし、上記の方法だけではありません。 「k」の正しい値は、次のような他の方法でも取得できます。
コードをコピーします コードは次のとおりです。 :

var data = [];

for (var k = 0; k (data[k] = function () {
alert (arguments.callee.x);
}).x = k; // k を関数の属性として使用します
}

// 結果も正しいです
data[0] (); // 0
data[1](); // 1
data[2](); // 2


別の機能がクロージャから返されます。 ECMAScript では、クロージャ内の return ステートメントは、制御フローを呼び出しコンテキスト (呼び出し元) に返します。 Ruby などの他の言語では、多くの形式のクロージャがあり、対応するクロージャの戻り値も異なります。次の方法が可能です。呼び出し元に直接返されることもあれば、場合によってはコンテキストから直接終了することもあります。 。

ECMAScript の標準終了動作は次のとおりです:
コードをコピー コードは次のとおりです:

function getElement() {

[1, 2, 3].forEach(function (要素) {

if (要素 % 2 == 0) {
// getElement に戻る代わりに関数 "forEach" に戻ります function
alert('found: ' element) // found: 2
return element; >
} );

return
}


ただし、ECMAScript の try catch によって次の効果を実現できます。 🎜>
コードをコピーします

コードは次のとおりです。 var $break = {}; function getElement( ) { try {
[1, 2, 3].forEach(function (要素) {

if (要素 % 2 == 0) {
// // getElement からの戻り値 "
alert('found: ' element); // 見つかった: 2
$break.data = element;
throw $break;
}

});

} catch (e) {
if (e == $break) {
return $break.data;
}
}

return null;
}

alert(getElement()); // 2


理論的バージョン
ここで、開発者がクロージャを次のように誤って理解していることを説明しましょう。親コンテキストから簡略化された内部関数を返すと、匿名関数のみがクロージャになり得ることも理解されます。

繰り返しますが、スコープ チェーンのため、すべての関数はクロージャです (関数の種類に関係なく、匿名関数、FE、NFE、FD はすべてクロージャです)。
関数の [[スコープ]] にはグローバル オブジェクトのみが含まれるため、関数コンストラクターを通じて作成された関数を除き、関数のタイプは 1 つだけです。

この問題をより明確にするために、ECMAScript のクロージャの定義の 2 つの正しいバージョンを示します。

ECMAScript では、クロージャは以下を指します。

理論的観点から: すべての機能。それらはすべて、作成時に上位コンテキストのデータを保存するためです。これは、関数内のグローバル変数へのアクセスは自由変数へのアクセスと同等であるため、単純なグローバル変数にも当てはまります。このとき、最も外側のスコープが使用されます。
実用的な観点から: 次の関数はクロージャとみなされます:
それが作成されたコンテキストが破棄された場合でもまだ存在します (たとえば、内部関数が親関数から返されます)
無料変数はコード内で参照されます
クロージャの実践的な使用法
実際に使用すると、クロージャは非常にエレガントなデザインを作成でき、funarg で定義されたさまざまな計算メソッドをカスタマイズできます。以下は、並べ替え条件関数をパラメータとして受け取る配列並べ替えの例です。




コードをコピー


コードは次のとおりです。 :
コピー コード


コードは次のとおりです:

[1, 2, 3].map(function (element) {
return element * 2;
} ); // [2, 4, 6]
関数パラメータを使用すると、検索メソッドを簡単に実装し、無制限の検索条件をサポートできます:
コードをコピー コードは次のとおりです。

someCollection.find(function (element) {
return element.someProperty == 'searchCondition';
});各配列要素に関数を適用する一般的な forEach メソッドなどのアプリケーション関数も含まれます。


コードをコピー コードは次のとおりです。 [1, 2, 3].forEach(function (element) {
if (element % 2 != 0) {
alert(element);
}
}); // 1, 3


ちなみに、関数オブジェクトの apply メソッドや call メソッドは、関数型プログラミングの応用関数としても使用できます。 apply と call は、「これ」について説明するときにすでに紹介されています。ここでは、それらをアプリケーション関数、つまり引数 (apply の引数リスト、call の個々の引数) に適用される関数として考えます。 🎜>
コードをコピーします

コードは次のとおりです: (function () { alert([] .join.call) (arguments, ';')); // 1;2;3 }).apply(this, [1, 2, 3]);
クロージャー もう 1 つ重要なことがあります。アプリケーション - 遅延呼び出し:



コードをコピー

コードは次のとおりです: var a = 10 ; setTimeout(function () { alert(a); // 10、1 秒後 }, 1000);

コールバックもあります Function



コードをコピー

コードは次のとおりです: //... var x = 10 ; // 例のみ xmlHttpRequestObject.onreadystatechange = function () {
// データの準備ができたときに呼び出されます
// ここでは、どのコンテキストで作成されたかに関係なく、
// この時点で変数 "x" の値はすでに存在します
alert(x) // 10
};


;また、カプセル化されたスコープを作成してヘルパー オブジェクトを非表示にすることもできます:




コードをコピーします


コードは次のとおりです:
var foo = {}; // 初期化(function (object) {
var x = 10;

object.getX = function _getX () {
return x;

})(foo);

alert(foo.getX()); // クロージャ「x」を取得します。


概要
この記事では、ECMAScript-262-3 に関する理論的な知識をさらに紹介します。これらの基本理論は、ECMAScript のクロージャーの概念を理解するのに役立つと思います。ご質問がございましたら、コメントにてお返事させていただきます。

その他の参考資料



JavaScript クロージャー (Richard Cornford 著)

フナルグ問題
    閉鎖
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。