web-dev-qa-db-ja.com

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

私の友人と私は現在、JSのクロージャーとそうでないものについて議論しています。本当に正しく理解していることを確認したいだけです。

この例を見てみましょう。カウントループがあり、カウンタ変数をコンソールに遅らせて出力します。したがって、setTimeoutクロージャーを使用してcounter変数の値を取得し、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を捉えるのにクロージャーを使っていないと彼に言いました、しかし彼は彼がそうであると主張します。私は彼がクロージャーをもう一つのsetTimeoutの中に入れることによって(=無名---を自分の匿名関数をsetTimeoutに渡すことによって)使わないことを証明しました。 。私のvarに彼の関数を格納しての後にループを実行しても10回印刷するなら、同じことが当てはまります。だから私の議論はそれです彼は実際にはcaptureiの値ではないので、彼のバージョンはnotになります。クロージャ。

私の試みは:

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

それで私はi(クロージャーの中でi2と命名される)を捕獲します、しかし今私はreturn他の関数を渡してこれを渡します。 私の場合、setTimeoutに渡された関数は実際にiをキャプチャします

今では誰がクロージャを使っていますか?誰がクロージャを使っていませんか?

どちらの解決策もコンソールの0から9までを遅らせるので元の問題は解決しますが、どちらの解決策でクロージャを使用するかを理解したいこれを達成するために。

541
leemes

closureの定義によれば、

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

関数の外側で定義されている変数を使用する関数を定義する場合は、closureを使用しています。 (私達は変数をa自由変数と呼ぶ)。
(1番目の例でも)それらはすべてclosureを使用しています。

94
kev

一言で言えば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を呼び出す

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

fg字句親と呼びます。前に説明したように、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内にいて、スコープfoo内で宣言されている変数fにアクセスしようとします。しかし、前に述べたように、ここでは字句親関数で宣言された変数にアクセスできます。 gfの字句親です。そのためhelloが表示されます。
それではconsole.log(bar);の行を見てみましょう。この時点で私たちはスコープf内にいて、スコープbar内で宣言されている変数gにアクセスしようとします。 barは現在のスコープ内で宣言されておらず、関数gfの親ではないため、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は、関数hg、およびfのスコープ内で宣言されているすべての変数にアクセスできます。これはクロージャで行われます。 JavaScriptでは、クロージャを使用すると、字句親関数、字句グランド親関数、字句グランド親関数で宣言されているすべての変数にアクセスできます。これはスコープチェーンと見なすことができます。字句親を持たない最後の親関数までのscope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...

ウィンドウオブジェクト

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

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

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

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

あなたがより詳細な説明を読んだ今、両方の解決策がクロージャを使うことは今や明らかかもしれません。しかし確かに、証明をしましょう。

新しいプログラミング言語を作りましょう。 JavaScript-No-Closure。名前が示すように、JavaScript-No-ClosureはClosuresをサポートしていないことを除けば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)
  })();
}

したがって、これはJavaScript-No-Closureにおいてundefinedを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);
}

したがって、これはJavaScript-No-Closureにおいてundefinedを10回印刷するでしょう。

両方のソリューションともクロージャを使用しています。

編集:これら3つのコードスニペットはグローバルスコープで定義されていないと仮定します。そうでなければ、変数fooiwindowオブジェクトにバインドされ、JavaScriptとJavaScript-No-Closureの両方のwindowオブジェクトを通してアクセス可能になります。

49
brillout

私は誰もがこれを説明する方法に満足していません。

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

クロージャがなければエラーになります

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

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

OuterFuncが架空のクロージャー無効バージョンのJavaScriptを返すと、outerVarへの参照はガベージコレクションされ、内部のfuncが参照するためのものは何も残されなくなります。

クロージャは基本的に、内部関数が外部関数の変数を参照するときにそれらの変数が存在することを可能にする特別な規則です。クロージャでは、参照されたvarsは外側の関数が終わった後でも維持されます。それがあなたがポイントを思い出すのを助けるならば 'closed'です。

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

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

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

しかし、参照されるコンテキストは実際には永続的であり、スナップショットのようなものではありません。外側の関数のローカル変数をインクリメントしてログに記録する高い値を警告し続けます。

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

あなたは両方ともクロージャを使っています。

私は ウィキペディアの定義 をここに取ります:

計算機科学では、クロージャー(レキシカルクロージャーまたは関数クロージャーとも呼ばれる)は、参照環境(その関数の各非局所変数(自由変数とも呼ばれる))への参照を格納する関数または関数への参照です。 。単純な関数ポインタとは異なり、クロージャーは、直接の字句スコープの外側で呼び出された場合でも、関数がこれらの非ローカル変数にアクセスすることを可能にします。

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

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

16
Jon

あなたとあなたの友人は両方ともクロージャを使います:

クロージャーは、機能とその機能が作成された環境という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);
}
12
Andrew D.

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

(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番目のアプローチは、動き回ったり一般化したりするのが簡単です。

9
Ja͢ck

クロージャ

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

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

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

これらの機能概念はあなたが使用するプログラミング言語/環境に強く依存することを覚えておいてください。 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)
}

すべてクロージャーを使用しています。実行のポイントをクロージャと混同しないでください。クロージャの 'スナップショット'が間違った瞬間にとられた場合、値は予想外のものになるかもしれませんが、確かにクロージャがとられます!

9
Andries

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

よく調べてみると、二人ともクロージャーを使っているようです。

あなたの友人の場合、iは無名関数1の内側でアクセスされ、i2console.logが存在する無名関数2の中でアクセスされます。

あなたの場合、あなたはi2が存在する無名関数の中でconsole.logにアクセスしています。 debugger;の前にconsole.logステートメントを追加し、クロム開発者向けツールの "Scope variables"の下に変数がどのスコープの下にあるかを伝えます。

5
Ramesh

以下を検討してください。これは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'));

以下は "機能"自体 "を閉じながら
(それ自体、この後のスニペットは単一の指示対象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。 fの最後の定義はfunction(){ console.log(9) } - の前に 0が出力されます。

警告!クロージャの概念は初等プログラミングの本質からの強制的な気晴らしかもしれません。

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

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

3
ekim

私の例とクロージャーについての説明を共有したいと思います。私は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つの図です。

メーカーから関数が返ってきたとき

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

関数がパラメータまたは非ローカル変数を介して呼び出される場合、コードには、margin_top、padding、およびa、b、nなどのローカル変数バインディングが必要です。機能コードが機能することを保証するために、ずっと前になくなったメーカー機能のスタックフレームがアクセス可能であるべきです。

0
Eunjung Lee