576

私の友人と私は現在、JS のクロージャとは何かについて話し合っています。本当に正しく理解していることを確認したいだけです。

この例を見てみましょう。カウント ループがあり、コンソールにカウンター変数を遅延させて出力したいと考えています。したがって、値 N の N 倍を出力しないように、setTimeoutクロージャーを使用してカウンター変数の値を取得します。

クロージャーまたはクロージャーに近いものがなければ、間違った解決策は次のようになります。

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

iもちろん、ループ後の値の 10 倍、つまり 10 が出力されます。

したがって、彼の試みは次のとおりです。

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

期待どおりに0から9を印刷します。

私は彼にをキャプチャするためにクロージャiを使用していないことを伝えましたが、彼は使用していると主張しています。私は、for ループ本体を別のループ内に入れ(匿名関数を に渡し)、10 かける 10 を再度出力することで、彼がクロージャを使用していないことを証明しました。彼の関数を a に格納し、ループのに実行すると、同じことが当てはまります。また、10 かける10 も出力します。したがって、私の主張は、彼は実際にはの値をキャプチャしていないため、彼のバージョンクロージャではありません。setTimeoutsetTimeoutvari

私の試みは:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

そのため、(クロージャ内でi名前を付けて)キャプチャしますが、別の関数を返し、これを渡します。私の場合、 setTimeout に渡された関数は実際にキャプチャします。i2i

では、クロージャーを使用しているのは誰で、使用していないのは誰でしょうか?

どちらのソリューションもコンソールに 0 から 9 を出力するのが遅れているため、元の問題は解決しますが、これら 2 つのソリューションのどちらがクロージャを使用してこれを達成しているかを理解したいと考えています。

4

12 に答える 12

669

編集者注:この投稿で説明されているように、JavaScript のすべての関数はクロージャーです。ただし、理論的な観点から興味深いこれらの関数のサブセットを特定することにのみ関心があります。これ以降、クロージャーという言葉への言及は、特に明記しない限り、この関数のサブセットを指します。

クロージャの簡単な説明:

  1. 関数を取ります。Fとしましょう。
  2. F のすべての変数をリストします。
  3. 変数には次の 2 つのタイプがあります。
    1. ローカル変数 (バインド変数)
    2. 非ローカル変数 (自由変数)
  4. F に自由変数がない場合、F はクロージャにはなりません。
  5. F に自由変数 (Fの親スコープで 定義されている) がある場合:
    1. 自由変数が束縛されるF の親スコープは 1 つだけでなければなりません。
    2. F がその親スコープの外から参照されている場合、それはその自由変数のクロージャーになります。
    3. その自由変数はクロージャ F の上位値と呼ばれます。

これを使用して、クロージャを使用する人と使用しない人を特定しましょう (説明のために、関数に名前を付けました)。

ケース 1: 友達のプログラム

for (var i = 0; i < 10; i++) {
    (function f() {
        var i2 = i;
        setTimeout(function g() {
            console.log(i2);
        }, 1000);
    })();
}

上記のプログラムには、 と の 2 つの関数がfありgます。それらがクロージャかどうか見てみましょう:

の場合f:

  1. 変数をリストします。
    1. i2ローカル変数です。
    2. i自由変数です。
    3. setTimeout自由変数です。
    4. gローカル変数です。
    5. console自由変数です。
  2. 各自由変数がバインドされている親スコープを見つけます。
    1. iグローバルスコープにバインドされています。
    2. setTimeoutグローバルスコープにバインドされています。
    3. consoleグローバルスコープにバインドされています。
  3. 関数はどのスコープで参照されていますか? グローバルスコープ 。
    1. したがってi、 はによって閉じられfません。
    2. したがってsetTimeout、 はによって閉じられfません。
    3. したがってconsole、 はによって閉じられfません。

したがって、関数fはクロージャーではありません。

の場合g:

  1. 変数をリストします。
    1. console自由変数です。
    2. i2自由変数です。
  2. 各自由変数がバインドされている親スコープを見つけます。
    1. consoleグローバルスコープにバインドされています。
    2. i2のスコープにバインドfれています。
  3. 関数はどのスコープで参照されていますか? のスコープsetTimeout
    1. したがってconsole、 はによって閉じられgません。
    2. したがって、i2によって閉じられgます。

したがって、この関数は、内から参照されたときg自由変数i2( の上位値g)のクロージャです。setTimeout

あなたに悪い:あなたの友人は閉鎖を使用しています。内部関数はクロージャーです。

ケース 2: あなたのプログラム

for (var i = 0; i < 10; i++) {
    setTimeout((function f(i2) {
        return function g() {
            console.log(i2);
        };
    })(i), 1000);
}

上記のプログラムには、 と の 2 つの関数がfありgます。それらがクロージャかどうか見てみましょう:

の場合f:

  1. 変数をリストします。
    1. i2ローカル変数です。
    2. gローカル変数です。
    3. console自由変数です。
  2. 各自由変数がバインドされている親スコープを見つけます。
    1. consoleグローバルスコープにバインドされています。
  3. 関数はどのスコープで参照されていますか? グローバルスコープ 。
    1. したがってconsole、 はによって閉じられfません。

したがって、関数fはクロージャーではありません。

の場合g:

  1. 変数をリストします。
    1. console自由変数です。
    2. i2自由変数です。
  2. 各自由変数がバインドされている親スコープを見つけます。
    1. consoleグローバルスコープにバインドされています。
    2. i2のスコープにバインドfれています。
  3. 関数はどのスコープで参照されていますか? のスコープsetTimeout
    1. したがってconsole、 はによって閉じられgません。
    2. したがって、i2によって閉じられgます。

したがって、この関数は、内から参照されたときg自由変数i2( の上位値g)のクロージャです。setTimeout

よかった:クロージャを使用しています。内部関数はクロージャーです。

したがって、あなたとあなたの友人の両方がクロージャーを使用しています。議論を停止します。閉鎖の概念と、あなたの両方のためにそれらを識別する方法をクリアしたことを願っています.

編集:なぜすべての関数がクロージャであるかについての簡単な説明(@Peterのクレジット):

まず、次のプログラムを考えてみましょう (これはコントロールです):

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. 上記の定義から、と の両方がクロージャlexicalScopeではないことがわかっています。regularFunction
  2. プログラムを実行すると、はクロージャーではないため(つまり、を含む親スコープ内のすべての変数にアクセスできるため)、アラートが発生することが予想 されます。message regularFunctionmessage
  3. プログラムを実行すると、実際にアラートが発生することがわかります。message

次に、次のプログラムを考えてみましょう (これはの方法です)。

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. 上記の定義から、closureFunction onlyがクロージャであることがわかります。
  2. プログラムを実行すると、クロージャーであるため、アラートが発生しないことが期待されます(つまり 、関数が作成された時点ですべての非ローカル変数にのみアクセスできます(この回答を参照)-これには含まれません)。message closureFunctionmessage
  3. プログラムを実行すると、実際にアラートが発生していることがわかります。message

これから何を推測しますか?

  1. JavaScript インタープリターは、他の関数を処理する方法とは異なる方法でクロージャーを処理しません。
  2. すべての関数は、そのスコープ チェーンを持ちます。クロージャーには、個別の参照環境がありません。
  3. クロージャーは、他のすべての関数と同じです。興味深いケースであるため、それらが属するスコープ外のスコープで参照される場合、単にそれらをクロージャーと呼びます。
于 2012-10-17T10:01:32.337 に答える
98

closure定義によると:

「クロージャー」とは、自由変数をそれらの変数をバインドする (式を「閉じる」)環境と一緒に持つことができる式 (通常は関数) です。

closure関数の外部で定義された変数を使用する関数を定義する場合に使用しています。(変数を自由変数と呼びます)。
それらはすべて使用しますclosure(最初の例でも)。

于 2012-10-17T08:59:40.700 に答える
55

一言で言えばJavascript クロージャは、関数がlexical-parent function で宣言され変数にアクセスできるようにします。

詳しい説明を見てみましょう。クロージャを理解するには、JavaScript がどのように変数をスコープするかを理解することが重要です。

スコープ

JavaScript では、スコープは関数で定義されます。すべての関数は新しいスコープを定義します。

次の例を考えてみましょう。

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

fプリントを呼び出す

hello
hello
2
Am I Accessible?

g別の関数内で定義された関数がある場合を考えてみましょうf

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

fレキシカルな親を呼び出しますg。前に説明したように、2 つのスコープがあります。スコープfとスコープg

しかし、一方のスコープはもう一方のスコープの「内」にあるので、子関数のスコープは親関数のスコープの一部ですか? 親関数のスコープで宣言された変数はどうなりますか。子関数のスコープからそれらにアクセスできますか? それがまさにクロージャーが介入する場所です。

閉鎖

JavaScript では、関数はスコープで宣言されたg変数にアクセスできるだけでなくg、親関数のスコープで宣言された変数にもアクセスできますf

以下を検討してください。

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

fプリントを呼び出す

hello
undefined

行を見てみましょうconsole.log(foo);。この時点でスコープ内にあり、スコープ内で宣言されてgいる変数にアクセスしようとします。しかし、前に述べたように、レキシカルな親関数で宣言された任意の変数にアクセスできます。のレキシカルな親です。したがって、印刷されます。 行を見てみましょう。この時点でスコープ内にあり、スコープ内で宣言されている変数にアクセスしようとします。は現在のスコープで宣言されておらず、関数は の親ではないため、未定義ですfoofgfhello
console.log(bar);fbargbargfbar

実際、レキシカルな「親の親」関数のスコープで宣言された変数にアクセスすることもできます。したがって、関数h内で定義された関数がある場合g

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

関数、、およびhのスコープで宣言されたすべての変数にアクセスできます。これはクロージャで行われます。JavaScriptクロージャーでは、レキシカル親関数、レキシカル親親関数、レキシカル親親関数などで宣言された任意の変数にアクセスできます。これはスコープ チェーンと見なすことができます。字句親を持たない最後の親関数まで。hgf scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...

ウィンドウ オブジェクト

実際、チェーンは最後の親関数で停止しません。もう 1 つ特別なスコープがあります。グローバルスコープ。関数で宣言されていないすべての変数は、グローバル スコープで宣言されていると見なされます。グローバル スコープには 2 つの特殊性があります。

  • グローバルスコープで宣言されたすべての変数は、どこからでもアクセスできます
  • グローバル スコープで宣言された変数は、windowオブジェクトのプロパティに対応します。

fooしたがって、グローバル スコープで変数を宣言するには、正確に 2 つの方法があります。関数で宣言しないかfoo、window オブジェクトのプロパティを設定します。

どちらの試みもクロージャーを使用しています

より詳細な説明を読んだので、両方のソリューションがクロージャを使用していることは明らかです。しかし念のため証明しておきましょう。

新しいプログラミング言語を作成しましょう。JavaScript-非閉鎖。名前が示すように、JavaScript-No-Closure は JavaScript と同じですが、Closure をサポートしていません。

言い換えると;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

では、JavaScript-No-Closure を使用した最初のソリューションで何が起こるか見てみましょう。

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

したがって、これはundefinedJavaScript-No-Closure で 10 回出力されます。

したがって、最初の解決策は閉鎖を使用します。

2 番目の解決策を見てみましょう。

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

したがって、これはundefinedJavaScript-No-Closure で 10 回出力されます。

どちらのソリューションもクロージャを使用しています。

編集: これら 3 つのコード スニペットは、グローバル スコープで定義されていないと想定されます。それ以外の場合、変数fooiはオブジェクトにバインドされるため、JavaScript と JavaScript-No-Closure の両方でオブジェクトをwindow介してアクセスできます。window

于 2012-10-17T09:33:55.437 に答える
22

私は誰もがこれを説明する方法に満足したことはありません。

クロージャを理解するための鍵は、クロージャなしでJSがどのようになるかを理解することです。

クロージャがないと、これはエラーをスローします

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

架空のクロージャーが無効なバージョンのJavaScriptでouterFuncが返されると、outerVarへの参照はガベージコレクションされ、内部関数が参照できるように何も残されません。

クロージャは本質的に、内部関数が外部関数の変数を参照するときにそれらの変数が存在することを可能にする特別なルールです。クロージャーを使用すると、参照される変数は、外部関数が実行された後、またはポイントを思い出すのに役立つ場合は「クローズ」された後でも維持されます。

クロージャがある場合でも、ローカルを参照する内部関数がない関数のローカル変数のライフサイクルは、クロージャのないバージョンの場合と同じように機能します。関数が終了すると、地元の人々はガベージコレクションを取得します。

内側の関数に外側の変数への参照があると、それはドアジャムがそれらの参照された変数のガベージコレクションの邪魔になるようなものです。

クロージャを調べるためのおそらくより正確な方法は、内部関数が基本的に内部スコープをそれ自体のスコープの基礎として使用することです。

ただし、参照されるコンテキストは実際には永続的であり、スナップショットのようではありません。返された内部関数を繰り返し起動して、外部関数のローカル変数をインクリメントおよびログに記録し続けると、より高い値を警告し続けます。

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
于 2012-10-18T01:51:44.840 に答える
17

あなたは両方とも閉鎖を使用しています。

ここでは、ウィキペディアの定義を使用します。

コンピューター サイエンスでは、クロージャー (レキシカル クロージャーまたは関数クロージャーとも呼ばれます) は、参照環境 (その関数の非ローカル変数 (自由変数とも呼ばれます) への参照を格納するテーブル) を伴う関数または関数への参照です。 . 単純な関数ポインターとは異なり、クロージャーを使用すると、直接のレキシカル スコープの外で呼び出された場合でも、関数は非ローカル変数にアクセスできます。

iあなたの友人の試みは、その値を取得し、コピーを作成してローカルに保存することにより、ローカルではない変数を明らかに使用していますi2

あなた自身の試みはi、(呼び出しサイトでスコープ内にある) 引数として無名関数に渡されます。これは今のところクロージャではありませんが、その関数は同じ を参照する別の関数を返しますi2。内部の無名関数i2はローカルではないため、クロージャが作成されます。

于 2012-10-17T09:08:52.787 に答える
13

あなたとあなたの友人の両方がクロージャーを使用しています。

クロージャは、関数とその関数が作成された環境の 2 つを組み合わせた特別な種類のオブジェクトです。環境は、クロージャーが作成された時点でスコープ内にあったローカル変数で構成されます。

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

function(){ console.log(i2); }匿名関数のクロージャ内で定義された友人のコード関数でfunction(){ var i2 = i; ... 、ローカル変数を読み書きできますi2

function(){ console.log(i2); }関数のクロージャー内で定義されたコード関数でfunction(i2){ return ...、ローカルの貴重な値を読み書きできますi2(この場合はパラメーターとして宣言されています)。

どちらの場合も、関数function(){ console.log(i2); }は に渡されsetTimeoutます。

もう 1 つの同等の (ただし、メモリ使用量は少ない) は次のとおりです。

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}
于 2012-10-17T09:03:01.227 に答える
10

両方の方法を見てみましょう。

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

setTimeout()独自のコンテキスト内で実行される無名関数を宣言し、すぐに実行します。の現在の値は、最初iにコピーを作成することによって保持されます。i2すぐに実行されるため、機能します。

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

内部関数の実行コンテキストを宣言します。これにより、の現在の値が;iに保持されます。i2このアプローチでは、値を保持するために即時実行も使用します。

重要

実行セマンティクスは、両方のアプローチ間で同じではないことに注意してください。あなたの内部関数はに渡されますがsetTimeout()、彼の内部関数はsetTimeout()それ自体を呼び出します。

両方のコードを別のコードでラップしてもsetTimeout()、2番目のアプローチだけがクロージャーを使用していることを証明するものではありません。そもそも同じことはありません。

結論

どちらの方法もクロージャを使用するため、個人的な好みになります。2番目のアプローチは、「移動」または一般化するのが簡単です。

于 2012-10-17T09:20:17.847 に答える
10

閉鎖

クロージャは関数でも式でもありません。これは、関数スコープ外で使用され、関数内で使用される変数からの一種の「スナップショット」と見なす必要があります。文法的には、「変数の閉包を取る」と言うべきです。

繰り返しますが、言い換えれば、クロージャーは、関数が依存する変数の関連コンテキストのコピーです。

もう一度 (ナイーフ): クロージャーは、パラメーターとして渡されていない変数にアクセスしています。

これらの機能概念は、使用するプログラミング言語/環境に大きく依存することに注意してください。JavaScript では、クロージャはレキシカル スコープに依存します (これはほとんどの C 言語に当てはまります)。

したがって、関数を返すことは、ほとんどの場合、匿名/名前のない関数を返します。関数がパラメーターとして渡されず、その (レキシカル) スコープ内で変数にアクセスする場合、クロージャーが取られます。

だから、あなたの例に関して:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

すべてクロージャーを使用しています。実行ポイントとクロージャーを混同しないでください。クロージャーの「スナップショット」が間違った時点で取得された場合、値は予期しないものになる可能性がありますが、確実にクロージャーが取得されます!

于 2012-10-17T09:49:27.417 に答える
9

これは、クロージャーとは何か、JS でどのように機能するかを思い出すために、少し前に書いたものです。

クロージャーは、呼び出されたときに、呼び出されたスコープではなく、宣言されたスコープを使用する関数です。JavaScript では、すべての関数がこのように動作します。スコープ内の変数値は、それらを指す関数が存在する限り存続します。このルールの例外は「this」です。これは、関数が呼び出されたときに内部にあるオブジェクトを参照します。

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 
于 2012-12-21T19:10:29.793 に答える
6

よく調べてみると、どちらも閉鎖を使用しているようです。

あなたの友人のケースでiは、匿名関数 1 内でi2アクセスされ、 が存在する匿名関数 2 でアクセスされますconsole.log

あなたの場合、存在するi2匿名関数内にアクセスしていますconsole.logdebugger;「スコープ変数」の下のクロム開発者ツールの前と中にステートメントを追加するconsole.logと、変数がどのスコープの下にあるかがわかります。

于 2012-10-17T08:58:44.597 に答える
4

以下を検討してください。fこれは、で終了する関数を作成および再作成しますiが、異なるものです!:

i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

以下は「a」関数「それ自体」で終了します
(自分自身!この後のスニペットは単一の指示対象を使用しますf

for(var i = 0; i < 10; i++) {
    setTimeout( new Function('console.log('+i+')'),  1000 );
}

またはより明確にするために:

for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}

注意。fis function(){ console.log(9) } before の最後の定義0が出力されます。

警告!クロージャの概念は、初等プログラミングの本質からの強制的な気晴らしになる可能性があります。

for(var i = 0; i < 10; i++) {     setTimeout( 'console.log('+i+')',  1000 );      }

x-refs .:
JavaScript クロージャーはどのように機能しますか?
Javascript
クロージャーの説明 (JS) クロージャーには関数内に関数が必要
ですか Javascript でクロージャーを理解するにはどうすればよいですか?
Javascript のローカル変数とグローバル変数の混乱

于 2015-03-15T13:56:13.053 に答える