JavaScriptのクローズドを深く理解する【訳】

13010 ワード

「上級プログラム設計」では、閉包についてはよく説明されていません。stackover flowで古い「JavaScript closure for dummies」(2016)を出しました。
出典:http://stackoverflow.com/questions/111102/how-do-javascript-closures-work
 
クローズドは魔法ではありません
本論文はJavaScriptコードを使って、プログラマにクローズドを理解させることを目的としています。
閉包の核心理念を理解しさえすれば、閉包は難しくないです。しかし、学術論文や学術に関する情報を勉強することによって、クローズドを理解するのは難しいです。
本文は主流言語プログラミングの経験があるプログラマーに送るものです。少なくとも以下のJavaScript関数を読むことができます。
1 function sayHello(name) {
2     var text = 'Hello ' + name;
3     var say = function() { console.log(text); }
4     say();
5 }
 
閉包例
二つの一言のまとめ:
  • クローズドは関数の局所変数であるが、関数return後も有効である
  • クローズドは、関数return後も解放されないスタック構造である(スタック構造が割り当てられているように)

  • 以下のコードは関数に参照を返します。
    1 function sayHello2(name) {
    2     var text = 'Hello ' + name; // Local variable
    3     var say = function() { console.log(text); }
    4     return say;
    5 }
    6 var say2 = sayHello2('Bob');
    7 say2(); // logs "Hello Bob"
    ほとんどのJavaScriptプログラマは、上記のコードはどのように変数(say 2)の関数への参照に戻りますか?理解できないなら、クローズドを学ぶ前に分かります。C言語プログラマは、この関数が関数の指針を返したと考えてもよく、変数sayとsay 2は関数を指す2つの指針であると考えています。
    Cの関数ポインタとJavaScriptの関数参照との間には、重要な違いがあります。JavaScriptでは、関数参照変数は関数を指すポインタでもあり、クローズドを指す隠蔽ポインタでもあると考えられます。
    匿名関数のため  function(){consolie.log(text)}は別の関数です。 sayHello 2() )中に宣言されているので、上のコードにはクローズドが含まれています。JavaScriptでは、他の関数で使用する場合は、 機能 キーワードでは、クローズドを作成します。
    Cおよびほとんどの他の一般的な言語では、1つの関数がリセットされた後、スタックが整理されているため、すべてのローカル変数がアクセスできなくなります。
    JavaScriptでは、他の関数で関数を宣言すると、関数を呼び出してもreturn後、この局所変数は依然としてアクセス可能であることを確認しました。このコードはsayHello 2の関数return後に関数sayを呼び出しました。
    function() { console.log(text); } // Output of say2.toString();
      say2.toString()         ,       text。  sayHello()     text         ,            'Hello Bob' text。
    魔法は、JavaScriptでは、関数が作成されたときにあるクローズドへの参照が暗黙的に行われ、方法の指針にオブジェクトへの暗黙的な参照が加えられているようなものです。
    その他の例
    いくつかの理由で閉め切ったのは理解しにくいかもしれません。しかし、いくつかの例を勉強すれば、彼らがどのように働いているのかが分かります。以下の例をよく研究してみてください。彼らがどのように動いているかが分かりません。閉包がどのように動いているかを完全に理解していないうちに彼らを使って、すぐに非常なものを作ってくれます。怪しいbug~
    例3
    この例は、ローカル変数が割り当てられておらず、参照によって保持されていることを示しています。これは外部関数が存在するときのように、スタック構造がメモリに保持されています。
    function say667() {
        // Local variable that ends up within closure
        var num = 666;
        var say = function() { console.log(num); }
        num++;
        return say;
    }
    var sayNumber = say667();
    sayNumber(); // alerts 667
     
    例4
    3つの大域関数は同じsetupSomeGlobals()を呼び出すクローズドにあるので、彼らは同じクローズドの同じ参照を指しています。
    var gLogNumber, gIncreaseNumber, gSetNumber;
    function setupSomeGlobals() {
        // Local variable that ends up within closure
        var num = 666;
        // Store some references to functions as global variables
        gLogNumber = function() { console.log(num); }
        gIncreaseNumber = function() { num++; }
        gSetNumber = function(x) { num = x; }
    }
    
    setupSomeGlobals();
    gIncreaseNumber();
    gLogNumber(); // 667
    gSetNumber(5);
    gLogNumber(); // 5
    
    var oldLog = gLogNumber;
    
    setupSomeGlobals();
    gLogNumber(); // 666
    
    oldLog() // 5
     
    3つの関数が定義されると、3つの関数は同じ閉じたパケットに対するアクセスを共有し、この閉じたパケットはsetupSomGlobals()の局所変数である。
    上記の距離において、再度呼び出したらsetupSomeGlobals()。 ,新しいクローズドを作成します。古い変数gAlertNumber、  gIncreaseNumber、  gSetNumber 。( JavaScript , , , )。例5
    多くの人にとって、この例では間違いがありますので、必ずクローズドを理解してください。ループの中で関数を定義するときは、クローズドの中の局部変数の表現は、通常はあなたが思っているほどではないので、十分に注意してください。
    function buildList(list) {
        var result = [];
        for (var i = 0; i < list.length; i++) {
            var item = 'item' + i;
            result.push( function() {console.log(item + ' ' + list[i])} );
        }
        return result;
    }
    
    function testList() {
        var fnlist = buildList([1,2,3]);
        // Using j only to help prevent confusion -- could use i.
        for (var j = 0; j < fnlist.length; j++) {
            fnlist[j]();
        }
    }
    行 result.push(function){consolie.log(item+'+list[i]] 3回の結果配列に匿名関数への参照を追加しました。匿名関数にあまり詳しくないなら、ここの匿名関数の役割は以下のコードに似ていると考えられます。
    pointer = function() {console.log(item + ' ' + list[i])};
    result.push(pointer);
    この例を実行すると、「item 2 undefined」はalertに3回も与えられます。これは前述のように、今回の3回はbuildListのローカル変数の同一のクローズドを指しているからです。  fnlist[j]() 実行時に匿名関数を呼び出しました。これらの匿名関数は同じクローズドを使用しています。このクローズドの現在値iとitem(iの値は3です。サイクルが終了したため、itemの値はitem 2です。インデックスは0から始まりますので、itemの値はitem 2、i+はiの値を3に加算します。
    例6
    本例では、クローズド・パケットの存在前の、このクローズドの外部関数内で宣言されたローカル変数は、クローズド・パケット中に含まれています。変数aliceは、実際には匿名関数の後に宣言されています。すなわち、匿名関数は先に宣言されています。aliceとクローズドは同じ作用領域にあるためです。同じように、sayAlice()がsayAlice()から返ってきた引用を直接呼び出しました。これは前の例と同じです。臨時変数がないだけです。
    function sayAlice() {
        var say = function() { console.log(alice); }
        // Local variable that ends up within closure
        var alice = 'Hello Alice';
        return say;
    }
    sayAlice()();
    変数sayもクローズド中にあり、sayAlice()に宣言された関数によってアクセスできます。また、関数内部の再帰的アクセスもできます。
    例7
    最後の例では、各コールはローカル変数に対して分離されたクローズドパケットを作成します。各関数の宣言は同じクローズドではなく、各関数の呼び出しには一つのクローズドがあります。
    function newClosure(someNum, someRef) {
        // Local variables that end up within closure
        var num = someNum;
        var anArray = [1,2,3];
        var ref = someRef;
        return function(x) {
            num += x;
            anArray.push(num);
            console.log('num: ' + num +
                '
    anArray ' + anArray.toString() + '
    ref.someVar ' + ref.someVar); } } obj = {someVar: 4}; fn1 = newClosure(4, obj); fn2 = newClosure(5, obj); fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4; fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4; obj.someVar++; fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5; fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
    Summaryまとめ
    すべてがよく分からないようであれば、これらの例を挙げて詳細に理解したほうがいいです。例を理解するよりも、説明を読むのがもっと難しいです。私はクローズドやスタック構造などの説明は技術的には厳密ではないですが、より簡単にクローズドを理解することができます。これらの基本理念を完全に把握してから、目をつけます。詳細はもっと役に立ちます。
    結論:
  • はいつでも別の関数で関数を使用して一つのクローズドを使用します。
  • は、いつでも関数内でeval()を使用して、一つのクローズドを使用しています。evalの内容は現在の関数の局所変数を指しています。同僚は、クローズド内でeval('var foo=...)を使って新しいローカル変数を作成することもできます。
  • は、関数にnew Fucntion(...)を使用すると、閉じたパケットは発生しません。(新しい関数は外部関数の局所変数を参照することができません。)
  • JavaScriptのクローズドは、すべてのローカル変数のコピーを保持しているように、関数の前に存在するときと同じです。
  • は、常に関数に入るときに作成され、関数内のすべての局所変数がこのクローズドに追加されると考えたほうがいい。
  • は、クローズドされた関数が呼び出されるたびに、新しいローカル変数のセットを保持します。(関数には内部宣言の関数が含まれています。また、バックまたは外部参照または他の何らかの方法で、内部関数への参照が含まれて保持されています。)
  • の関数は同じソーステキストを持っているように見えるかもしれませんが、彼らの「隠し」がクローズドされているため、全く異なる行動があるかもしれません。JavaScriptコードは実際には関数参照がクローズドされているかどうかを確認する方法がないと思います。
  • もし動的なソースコードの修正を試みているなら、 myFunction=Function(myFunction.toString).replace(/Hello/,'Hola') ),myFunctionが閉梱されている場合は、修正は有効ではないかもしれません。(もちろん実行中にソースコードの文字列を修正することも考えられますが、いつも特殊な場合があります。)
  • は関数内で関数宣言を行い、関数宣言の中で関数宣言を続けます。この方法は1つ以上の層のクローズドを得ることができます。
  • は、一般的にクローズドは関数と捕捉変数の範囲だと思いますが、ここではこの定義を使用していません。
  • JavaScriptのクローズドは、通常の関数式言語とは異なります。
  •  関連リンク
  • Duglas Crockford's simulted prvate atributes and prvate methods for an object,using closures.
  • A great explanion of how closures can cause memory leaks in IE if you are not careful.