JavaScriptクロージャーと無名関数


562

私の友人と私は現在、JSの閉鎖とは何か、閉鎖ではないことについて話し合っています。本当に正しく理解できるようにしたいだけです。

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

クロージャーまたはクロージャーに近いものがない場合の間違った解決策は次のとおりです。

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名前が付けられたi2)キャプチャーしますが、今度は別の関数を返し、これを渡します。私の場合、setTimeoutに渡された関数は実際にをキャプチャしiます。

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

どちらのソリューションもコンソールで0から9までの遅延を表示するため、元の問題は解決されますが、これらの2つのソリューションのどちらがクロージャ使用してこれを実現するのかを理解したいことに注意してください。


14
これらは役に立つかもしれません:「閉鎖」と「ラムダ」の違いは何ですか?そして、ラムダとは何ですか?
Blender、

1
@leemes:2番目のリンクについては、忍者の編集を参照してください。
Blender

2
契約を結んだところです。適切な担当者が、この質問に関連するSOポイントを獲得します
brillout

1
@leemes-どちらもクロージャーを使用しています。どちらも、外側の関数と内側の関数という2つの関数を作成しました。そして、両方の内部関数はクロージャーです。関数はすべてラムダ(無名関数)です。詳細については私の答えを読んでください。
Aadit M Shah 2012

1
@blesh-変更されたクロージャーが何であるか私にはわかりません。リンクがC#コードを指しているようです。変更されたクロージャーはJavaScriptでサポートされていますか?
Aadit M Shah

回答:


650

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

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

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

これを使用して、誰がクロージャーを使用し、誰がクロージャーを使用しないかを理解しましょう(説明のために、関数に名前を付けました)。

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

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

上記のプログラムにはf、およびの2つの関数があり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);
}

上記のプログラムにはf、およびの2つの関数があり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. 上記の定義から、両方がクロージャlexicalScoperegularFunctionはないことがわかります
  2. 我々はプログラムを実行すると、我々が期待する message警告することがあるため regularFunction閉鎖ではない(すなわち、それはへのアクセスがあるすべて -を含め、親スコープ内の変数をmessage)。
  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、クロージャーであることがわかります
  2. プログラムを実行するとき、クロージャーであるため、警告されないことが期待さ messageれます(つまり、プログラムは、すべての非ローカル変数にのみアクセスできます closureFunction、関数が作成さたときに、ません(この回答を参照)-これには含まれませんmessage)。
  3. プログラムを実行すると message実際にアラートが発生していることがわかります。

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

  1. JavaScriptインタープリターは、他の関数を処理する方法と異なる方法でクロージャーを処理しません。
  2. すべての関数は、スコープチェーンを伴います。クロージャーにはありません個別の参照環境。
  3. クロージャーは他のすべての関数と同じです。これは興味深いケースなので、それらが属しているスコープのスコープで参照されている場合、それらを単にクロージャと呼びます。

40
細かいところまで行き、何が起こっているのかを非常によく説明しているので、受け入れられました。そして最後に、クロージャーが何であるか、またはJSで変数バインディングがどのように機能するかを理解しました。
leemes

3
ケース1では、それgはのスコープで実行されsetTimeoutますが、ケース2では、それfはグローバルスコープで実行されます。どちらもsetTimeout内にあるので、違いは何ですか?
rosscj2533 2012年

9
この情報源を教えていただけますか?関数があるスコープで呼び出されても別のスコープでは呼び出されない場合、関数がクロージャになる可能性がある定義を見たことがありません。したがって、この定義は、私が慣れているより一般的な定義(kevの回答を参照)のサブセットのように見えます。
Briguy37 2012年

11
@AaditMShahクロージャーとは何かについては同意しますが、JavaScriptの通常の関数とクロージャーには違いがあるかのように話します。違いはありません; 内部的に、すべての関数は、それが作成された特定のスコープチェーンへの参照を伴います。JSエンジンは、これを別のケースとは見なしません。複雑なチェックリストは必要ありません。すべての関数オブジェクトが字句スコープを持っていることを知ってください。変数/プロパティがグローバルに利用可能であるという事実は、関数をもはやクロージャにしません(それは単なる役に立たないケースです)。
ピーター、

13
@ピーター-あなたは何を知っている、あなたは正しいです。通常の関数とクロージャーの間に違いはありません。私はこれを証明するためにテストを実行しました、そしてそれはあなたの好意に帰着します:ここにコントロールがあり、ここに代替があります。あなたの言うことは理にかなっています。JavaScriptインタープリターは、クロージャーについて特別な簿記を行う必要があります。これらは、ファーストクラスの機能を持つレキシカルスコープの言語の副産物です。私の知識は私が読んだものに限られていました(これは誤りでした)。訂正していただきありがとうございます。同じことを反映するように回答を更新します。
Aadit M Shah 2012

96

closure定義によると:

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

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


1
3番目のバージョンは、関数の外部で定義された変数をどのように使用しますか?
2012年

1
@Jonでは、i2外部で定義された関数が使用されます。
kev 2012年

1
@kev関数の外部で定義された変数を使用する関数を定義する場合は、クロージャーを使用しています......「Aadit M Shah」の「Case 1:Your Friend's Program」で、答えは「function f」です閉鎖?i(関数の外部で定義される変数)を使用します。グローバルスコープは決定子を参照しますか?
内部


54

一言で言えばJavascriptのクロージャする機能できるように変数にアクセスされる字句-親関数内で宣言を

より詳細な説明を見てみましょう。クロージャを理解するには、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 printsの呼び出し

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

しかし、1つのスコープは他のスコープの「内」にあるので、子関数のスコープは親関数のスコープの一部ですか?親関数のスコープで宣言された変数で何が起こるか。子関数のスコープからそれらにアクセスできますか?それはまさにクロージャーが介入するところです。

閉鎖

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 printsの呼び出し

hello
undefined

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

実際、字句「大親」関数のスコープで宣言された変数にアクセスすることもできます。したがって、関数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すべての変数にアクセスできるようになる機能の範囲内で宣言hgf。これはクロージャで行われます。JavaScript クロージャでは、レキシカル親関数、レキシカルグランド親関数、レキシカルグランドグランド親関数などで宣言された変数にアクセスできます。これはスコープチェーン見なすことができます。 scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... 字句の親を持たない最後の親関数まで。

ウィンドウオブジェクト

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

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

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

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

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

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

言い換えると;

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つのコードスニペットはグローバルスコープで定義されていないことが前提です。それ以外の場合は、変数fooとするiと結合することになるwindowオブジェクトので、介してアクセス可能なwindowJavaScriptとJavaScriptの-NO-クロージャーの両方でオブジェクト。


なぜi未定義でなければならないのですか?親スコープを参照するだけで、クロージャーがなかった場合でも有効です。
leemes

fooがJavaScript-No-Closureで定義されていないのと同じ理由で。<code> i </ code>は、字句親で定義された変数にアクセスできるJavaScriptの機能により、JavaScriptで未定義ではありません。この機能はクロージャーと呼ばれます。
brillout 2012年

すでに定義されている変数と自由変数を参照することの違いを理解していませんでした。クロージャでは、外部コンテキストでバインドする必要がある自由変数を定義します。コードでは、関数を定義するときにに設定 i2iます。これはi自由変数ではありません。それでも、私たちはあなたの関数をクロージャーと見なしますが、自由変数がないため、それがポイントです。
leemes 2012年

2
@leemes、同意する。そして、受け入れられた答えと比較して、これは実際に何が起こっているかを実際に示していません。:)
Abel

3
私はこれが最良の答えだと思います。閉鎖を一般的かつ簡単に説明してから、特定のユースケースに入りました。ありがとう!
ティム・ピーターソン2013年

22

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

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

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

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

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

閉鎖が無効になっている架空のバージョンのJavaScriptでouterFuncが返されると、outerVarへの参照はガベージコレクションされ、内部funcが参照するものは何も残されません。

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

クロージャがあっても、ローカルを参照する内部関数のない関数でのローカル変数のライフサイクルは、クロージャなしのバージョンと同じように機能します。機能が終了すると、地元の人々はゴミを収集します。

内部funcで外部変数への参照を取得すると、参照された変数のガベージコレクションの邪魔になるようなドア枠ができます。

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

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

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

あなたはそれによって「スナップショット」(私はあなたが私の答えを参照していると思う)について正しいです。私はその行動を示す言葉を探していました。あなたの例では、それは「ホットリンク」クロージャー構造として見ることができます。内部関数でパラメーターとしてクロージャーをキャッチするとき、「スナップショット」として動作することを示すことができます。しかし、私は同意します、誤用された言葉は主題に混乱を追加するだけです。それについて何か提案があれば、私は私の答えを更新します。
Andries

内部関数に名前付き関数を指定すると、説明に役立つ場合があります。
Phillip Senn 2013年

クロージャーがないと、存在しない変数を使用しようとするため、エラーが発生します。
ファンメンデス

うーん...良い点。未定義の変数を参照すると、最終的にグローバルオブジェクトのプロパティとして検索されるため、エラーがスローされませんでしたか、それとも未定義の変数への割り当てと混同していますか?
Erik Reppen、2015

17

両方ともクロージャを使用しています。

I「mは、一緒に行くウィキペディアの定義をここに:

コンピュータサイエンスでは、クロージャ(字句クロージャまたは関数クロージャ)は、関数または関数への参照と参照環境(その関数の各非ローカル変数(フリー変数とも呼ばれます)への参照を格納するテーブル)を一緒にしたものです。クロージャーは、単純な関数ポインターとは異なり、直接の字句スコープの外で呼び出された場合でも、関数がそれらの非ローカル変数にアクセスできるようにします。

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

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


ええ、でもポイントは彼がそれをやっている方法だと思います。彼は単ににコピーしてiからi2、いくつかのロジックを定義し、この関数を実行します。すぐには実行せず、varに格納し、ループの後に実行すると、10が出力されますね。だから私捕らえませんでした
leemes

6
@leemes:iうまく捉えました。説明している動作は、閉鎖と非閉鎖の結果ではありません。それはクローズされた変数がその間に変更された結果です。関数をすぐに呼び出してi引数として渡すことで、異なる構文を使用して同じことを行っています(現在の値をその場でコピーします)。あなたが自分のsetTimeoutものを別setTimeoutのものの中に置くと、同じことが起こります。
2012年

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ます。

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

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

1
あなたのソリューションと私の友人のソリューションが「より高速で、メモリ使用量が少ない」理由がわかりません。詳しく説明してください。
brillout 2012年

3
ソリューションでは、20個の関数オブジェクトを作成します(各ループに2つのオブジェクト:2x10 = 20)。あなたの友人の解決策と同じ結果。「my」ソリューションでは、11個の関数オブジェクトのみが作成されます。1つはforループの前、10個は「内部」-1 + 1x10 = 11です。その結果、メモリ使用量が減り、速度が向上します。
Andrew D.

1
理論的には、それは本当です。実際には、次も参照してください。このJSPerfベンチマークを参照してください:jsperf.com/closure-vs-name-function-in-a-loop/2
Rob W

10

閉鎖

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

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

もう一度(naïf):クロージャーがパラメーターとして渡されていない変数にアクセスしています。

これらの関数の概念は、使用するプログラミング言語/環境に強く依存することに注意してください。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)
}

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



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番目の方法は、「移動」または一般化する方が簡単です。


違いは次のとおりだと思います。彼の解決策(1番目)は参照によってキャプチャされ、私のもの(2番目)は値によってキャプチャされます。この場合、違いはありませんが、実行を別のsetTimeoutに入れると、彼の解決策には、現在ではなく、iの最終値を使用するという問題があることがわかります。現在の値(値によってキャプチャされた後)。
leemes

@leemesどちらも同じ方法でキャプチャします。関数の引数または代入を介して変数を渡すことは同じことです...実行を別の方法でラップする方法を質問に追加できますsetTimeout()か?
ジャック

これを確認してみましょう...関数オブジェクトを渡すことができ、元の変数iを変更できることを示しました。関数を実行する場所や時間に依存せず、出力される関数に影響を与えません。
leemes

待って、あなたは(外側の)setTimeoutに関数を渡さなかった。それらを削除して()、関数を渡すと、出力の10倍が表示されます10
leemes

@leemes前に述べたように、これ()がまさにあなたのように彼のコードを機能させるもの(i)です。彼のコードをラップするだけでなく、変更を加えたので、有効な比較を行うことができなくなりました。
ジャック

8

これは、クロージャーとは何か、そしてそれが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' 

6

よく調べたところ、どちらもクロージャを使用しているようです。

お友達の場合、i無名関数1内でi2アクセスされ、console.logが存在する無名関数2でアクセスされます。

あなたのケースi2console.logは、が存在する匿名関数内にアクセスしています。「スコープ変数」の下のchrome開発ツールのdebugger;console.logと中にステートメントを追加します。これにより、変数のスコープがわかります。


2
右側のパネルの「閉鎖」セクションは、これ以上具体的な名前がないために使用されています。「ローカル」は「閉鎖」よりも強い指標です。
Rob W


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」関数「itself」で終了します
(自分自身!この後のスニペットは単一の参照先を使用します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 );
}

NB。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のローカル変数とグローバル変数の混乱


スニペットが初めて試された-コントロール方法がわからない- Run' only was desired - not sure how to remove the コピー `
ekim

-1

私の例と閉鎖についての説明を共有したいと思います。Pythonの例と、スタックの状態を示す2つの図を作成しました。

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n * margin_top, a * n, 
            ' ‘ * padding, msg, '  * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)

f('hello')
g(‘good bye!')

このコードの出力は次のようになります。

*****      hello      #####

      good bye!    ♥♥♥

以下は、関数オブジェクトにアタッチされたスタックとクロージャを示す2つの図です。

関数がメーカーから返されたとき

関数が後で呼び出されたとき

関数がパラメーターまたは非ローカル変数を介して呼び出される場合、コードには、a、b、nだけでなくmargin_top、パディングなどのローカル変数バインディングが必要です。関数コードが機能することを保証するために、ずっと前になくなったメーカー関数のスタックフレームにアクセスできる必要があります。これは、関数メッセージオブジェクトとともに見つけることができるクロージャーにバックアップされています。


この回答を削除したいと思います。問題は閉鎖とは何かに関するものではないことがわかったので、別の質問に移ります。
ウンジュンリー

2
自分のコンテンツを削除する機能があると思います。delete回答の下のリンクをクリックします。
Rory McCrossan
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.