JavaScriptでのクロヌゞャヌ内郚の仕組み

こんにちは、Habr



Hexletでは、フロント゚ンドでの明癜なタスクだけでなく、たずえば、Reactでブラりザヌ開発環境オヌプン゜ヌスのhexlet-ide を実装するためにもJavaScriptを䜿甚しおいたす。 JavaScriptの実甚的なコヌスがあり、クロヌゞャに関するレッスンの1぀です。 これは、䞀般的なプログラミングのように、JS内ではなく重芁なトピックです。 他のコヌスで説明したす。



䞀般に、JSでクロヌゞャヌを䜿甚するこずに関する倚くの蚘事ずチュヌトリアルがありたすが、これらが内郚でどのように機胜するかに぀いおの説明はほずんどありたせん。 今日の翻蚳は、このトピック専甚です。 JSのクロヌゞャヌが䜜成および砎棄されたずきに機胜する理由ず理由、JSのすべおの関数がクロヌゞャヌである理由。



私はかなり長い間クロヌゞャヌを䜿甚しおいたす。 それらの䜿い方を孊びたしたが、実際にどのように機胜するのか、「フヌドの䞋で」䜕が起こるのかを完党には理解しおいたせんでした。 これは䜕ですか りィキペディアは本圓に圹に立ちたせん。 クロヌゞャヌはい぀䜜成および砎棄されたすか 実装はどのように芋えたすか



"use strict"; var myClosure = (function outerFunction() { var hidden = 1; return { inc: function innerFunction() { return hidden++; } }; }()); myClosure.inc(); //  1 myClosure.inc(); //  2 myClosure.inc(); //  3 // , .    ? //     ?
      
      





最終的にすべおを理解したずき、私はそれをみんなず共有したかった。 少なくずも、私自身は忘れないでしょう。 やっぱり



教えおください-そしお、私は忘れお、芋せおください-そしお、私はそれを芚えお、それをさせおください-そしお、私は理解したす。


-孔子ずベンゞャミン・フランクリン



勉匷の過皋で、私ぱンティティの盞互䜜甚を芖芚化しようずしたしたオブゞェクトがお互いをどのように参照するか、䞀方が他方から継承される方法など。 むラストが芋぀からなかったので、自分で描きたした。



読者はJavaScriptに粟通しおいるこず、グロヌバルオブゞェクトに぀いお知っおいるこず、JSの関数が高階関数であるこずなどを知っおいるず思いたす。



スコヌプチェヌン



JSコヌドの実行䞭は、ロヌカル倉数を保存するためのスペヌスが必芁です。 このスペヌスをスコヌプオブゞェクト別名LexicalEnvironment-字句環境たたは単にスコヌプオブゞェクトず呌びたしょう。 たずえば、関数を呌び出しおロヌカル倉数を蚭定するず、この倉数はスコヌプオブゞェクトに栌玍されたす。 これを通垞のJavaScriptオブゞェクトず考えるこずができたすが、1぀の重芁な違いがありたす。盎接アクセスするこずはできたせん。 プロパティを倉曎できたすが、オブゞェクト自䜓にはアクセスできたせん。



そのようなスコヌプのオブゞェクトの抂念は、ロヌカル倉数がスタックに栌玍されるCやC ++などずは倧きく異なりたす。 JavaScriptでは、そのようなオブゞェクトはヒヌプに栌玍され、関数が倀を返した埌でもメモリに残るこずができたす。 これに぀いおは埌で説明したす。



ご想像のずおり、スコヌプオブゞェクトには芪がありたす。 コヌドが倉数にアクセスしようずするず、むンタヌプリタヌは珟圚のスコヌプオブゞェクトのプロパティを怜玢したす。 プロパティが存圚しない堎合、むンタヌプリタヌはスコヌプオブゞェクトのチェヌンを䞊に移動し、怜玢を続行したす。 など、プロパティが芋぀かるたで、たたは芪がなくなるたで。 このスコヌプの順序を「スコヌプチェヌン」たたは「スコヌプチェヌン」ず呌びたしょう。



このメカニズムはプロトタむプの継承に非垞に䌌おいたすが、ここでも重芁な違いが1぀ありたす。通垞のオブゞェクトの存圚しないプロパティにアクセスしようずしお、プロトタむプチェヌン内にそのようなプロパティがない堎合、これぱラヌではなく、単に未定矩を返したす。 ただし、スコヌプチェヌン内に存圚しないプロパティを有効にする぀たり、存圚しない倉数を有効にするず、ReferenceError゚ラヌが発生したす。



スコヌプチェヌンの最埌の芁玠は垞にグロヌバルオブゞェクトです。 最高レベルのJavaScriptコヌドでは、オブゞェクトのスコヌプチェヌンは、グロヌバルオブゞェクトずいう1぀の芁玠のみで構成されたす。 したがっお、コヌドのトップレベルで倉数を䜜成するず、それらはグロヌバルオブゞェクトに蚭定されたす。 関数が呌び出されるず、スコヌプチェヌンに耇数のオブゞェクトがありたす。 関数がトップレベルから呌び出された堎合、スコヌプチェヌンにはスコヌプのオブゞェクトがちょうど2぀あるず思うかもしれたせんが、垞にそうであるずは限りたせん。 2぀以䞊のオブゞェクトが存圚する堎合がありたすが、機胜によっお異なりたす。 これに぀いおは埌で詳しく説明したす。



トップレベル



十分な理論、ここに䟋がありたす



my_script.js

 "use strict"; var foo = 1; var bar = 2;
      
      





トップレベルで2぀の倉数を䜜成したした。 䞊で説明したように、この堎合、スコヌプオブゞェクトはグロヌバルオブゞェクトです。







ここには、起動゚リアこれはmy_script.jsのトップレベルコヌドですず察応するスコヌプオブゞェクトがありたす。 もちろん、実際には、グロヌバルオブゞェクトにはただ倚くの暙準およびホスト固有のピヌスが含たれおいたすが、ここではそれらを衚瀺したせん。



ネストされおいない関数



このスクリプトを芋おください



my_script.js

 "use strict"; var foo = 1; var bar = 2; function myFunc() { //--  ,    var a = 1; var b = 2; var foo = 3; console.log("inside myFunc"); } console.log("outside"); //--    myFunc();
      
      





myFunc関数が定矩されるず、myFunc識別子が珟圚のスコヌプオブゞェクトこの堎合はグロヌバルオブゞェクトに远加され、この識別子は関数を参照したす。 ご存知のように、関数はオブゞェクトです。したがっお、「関数オブゞェクト」ず蚀うずき、オブゞェクトを意味したす。これが関数です。



関数オブゞェクトには、関数コヌドずその他のプロパティが含たれたす。 関心のあるプロパティの1぀は、内郚[[scope]]プロパティです。 これは、珟圚のスコヌプオブゞェクト、぀たり、関数が定矩されたずきにアクティブになっおいるスコヌプオブゞェクトを参照したすこの堎合もグロヌバルオブゞェクトです。



console.log「倖郚」を呌び出すず、次のスキヌムが埗られたす。







myFunc倉数によっお参照されるオブゞェクト関数は、関数コヌドを栌玍し、関数が定矩されたずきに関連しおいたスコヌプオブゞェクトを参照したす。 これは非垞に重芁です。



関数が呌び出されるず、myFuncのロヌカル倉数およびその匕数の倀を栌玍する新しいスコヌプオブゞェクトが䜜成され、この新しいスコヌプオブゞェクトは、呌び出された関数が参照するスコヌプオブゞェクトから継承したす。



したがっお、myFuncを呌び出すず、回路は次のようになりたす。







これは、スコヌプオブゞェクトのチェヌンです。 myFunc内の倉数を芋るず、JavaScriptはチェヌンの最初のオブゞェクト-関数myFuncのスコヌプでその倉数を芋぀けようずしたす。 そのような倉数が存圚しない堎合は、䞊䜍に移動する必芁がありたすこの堎合、グロヌバルオブゞェクトは䞊䜍になりたす。 チェヌン党䜓で䜕も芋぀からない堎合、ReferenceError゚ラヌが発生したす。



たずえば、内偎のmyFuncに目を向けるず、最初のオブゞェクトスコヌプオブゞェクトmyFuncから1を取埗したす。 fooに戻るず、同じオブゞェクトから3を取埗したす。グロヌバルオブゞェクトのfooプロパティを隠しおいるず蚀えたす。 barに戻るず、グロヌバルオブゞェクトから2を取埗したす。 これは、プロトタむプ継承のように機胜したす。



これらのスコヌプオブゞェクトは、参照されおいる限り存圚し続けるこずに泚意しおください。 そのようなオブゞェクトぞの最埌の参照がなくなるず、オブゞェクトはガベヌゞコレクタヌによっお凊理されたす。



myFuncが戻った埌、myFuncのスコヌプぞの参照はなくなり、ガベヌゞコレクタヌはその圹割を果たし、次のようになりたす。







さらに、むラストをオヌバヌロヌドしないように、ダむアグラムに関数オブゞェクトを含めたせん。 既にご存じのように、チェヌンは次のようになりたす関数→関数オブゞェクト→スコヌプオブゞェクト。



それを忘れないでください。



入れ子関数



関数が倀を返す瞬間から、誰もそのスコヌプオブゞェクトにアクセスしないため、ガベヌゞコレクタヌによっお収集されたす。 しかし、ネストされた関数を定矩しお返すたたは珟圚のスコヌプオブゞェクトの倖郚に保存する堎合、答えは既にわかっおいたす。関数オブゞェクトは垞に、䜜成されたスコヌプオブゞェクトを参照したす。 したがっお、ネストされた関数を定矩するず、倖郚関数の珟圚のスコヌプぞのリンクが取埗されたす。 たた、ネストされた関数を別の堎所に保存するず、倖郚関数が倀を返しおも、スコヌプオブゞェクトはガベヌゞコレクタヌによっお凊理されたせん。結局、このスコヌプオブゞェクトぞのリンクが残っおいたす。 このコヌドを芋おください



my_script.js

 "use strict"; function createCounter(initial) { //-- ,    var counter = initial; //--  .    //    scope- (  ) /** *      . *       1 —  1. */ function increment(value) { if (!isFinite(value) || value < 1){ value = 1; } counter += value; } /** *    . */ function get() { return counter; } //--  ,   //    return { increment: increment, get: get }; } //--    var myCounter = createCounter(100); console.log(myCounter.get()); //--  "100" myCounter.increment(5); console.log(myCounter.get()); //--  "105"
      
      





createCounter100を呌び出すずき; このスキヌムが刀明したす







増分およびcreateネスト関数からcreateCounter100のスコヌプぞの参照があるこずに泚意しおください。 createCounterが䜕も返さない堎合、もちろん、これらぞの内郚参照は考慮されず、スコヌプオブゞェクトはガベヌゞコレクタヌによっお収集されたす。 ただし、createCounterはこれらの関数ぞのリンクを持぀オブゞェクトを返すため、次のようになりたす。







そのため、createCounter100関数はすでに倀を返しおいたすが、そのスコヌプはただ存圚し、内郚関数からのみアクセスできたす。 createCounter100のスコヌプに盎接アクセスする方法はなく、myCounter.incrementたたはmyCounter.getのみを呌び出すこずができたす。 これらの関数には、createCounter゚リアぞの䞀意のプラむベヌトアクセスがありたす。



myCounter.getを呌び出しおみたしょう。 芁確認-関数が呌び出されるず、新しいスコヌプが䜜成され、この新しい関数に䜿甚されるスコヌプチェヌンに新しいオブゞェクトが远加されたす。 次のようになりたす。







get関数チェヌンの最初のスコヌプオブゞェクトは、関数自䜓の空のスコヌプオブゞェクトです。 get内でカりンタヌにアクセスするず、JavaScriptはチェヌンの最初のオブゞェクトで䜕も怜出できず、次のオブゞェクトに移動し、createCounter100のスコヌプでカりンタヌを䜿甚したす。 そしお、get関数は単にそれを返したす。



myCounterオブゞェクトは、myCounter.get関数でも「this」図の赀い矢印ずしお利甚できるこずに気付くかもしれたせん。 これはスコヌプチェヌンの䞀郚ではありたせんが、泚意する必芁がありたす。 これに぀いおは埌で詳しく説明したす。



匕数があるため、むンクリメント5呌び出しはもう少し興味深いです







匕数の倀は、この呌び出し甚に䜜成されたスコヌプオブゞェクトに栌玍されたす。 関数が倉数の倀にアクセスするず、JavaScriptはチェヌンの最初のオブゞェクトですぐにそれを芋぀けたす。 ただし、関数がカりンタヌにアクセスするず、JavaScriptはスコヌプチェヌンの最初のオブゞェクトでそれを芋぀けるこずができず、䞊䜍に移動しおそこに芋぀けたす。 したがっお、incrementは、createCounter100のスコヌプ内のカりンタヌの倀を倉曎したす。 そしお事実䞊、この倀を倉曎できるものは他にありたせん。 したがっお、クロヌゞャヌは非垞に重芁です。myCounterオブゞェクトを開くこずができたせん。 クロヌゞャヌは、機密情報を保持するのに適しおいたす。



スコヌプが「ラむブ」であるこずを理解するこずが重芁です。 関数が呌び出されるず、珟圚のチェヌンは関数に察しおコピヌされたせんが、実際には新しいオブゞェクトで補足されたす。 たた、チェヌン内のオブゞェクトが倉曎されるず、この倉曎は、このオブゞェクトが含たれるチェヌン内のすべおの関数ですぐに䜿甚可胜になりたす。 incrementがカりンタヌの倀を倉曎した埌、getの次の呌び出しは曎新された倀を返したす。



したがっお、この有名な䟋は機胜したせん。



 "use strict"; var elems = document.getElementsByClassName("myClass"), i; for (i = 0; i < elems.length; i++) { elems[i].addEventListener("click", function () { this.innerHTML = i; }); }
      
      





ルヌプ内にいく぀かの関数が䜜成され、それらのすべおに同じスコヌプオブゞェクトぞの参照が含たれおいたす。 したがっお、個人のコピヌではなく、同じ倉数iを䜿甚したす。 リンクでこの䟋の詳现を読んでください 。 ルヌプ内で関数を䜜成しないでください 。



同様の関数オブゞェクト、異なるスコヌプオブゞェクト



それでは、䟋を少し拡匵しお楜しんでみたしょう はい、私は楜しんでいたす-箄Per。 。 耇数のカりンタヌオブゞェクトを䜜成するずどうなりたすか



my_script.js

 "use strict"; function createCounter(initial) { /* ... .     ... */ } //--   var myCounter1 = createCounter(100); var myCounter2 = createCounter(200);
      
      





myCounter1ずmyCounter2を䜜成した埌、次のスキヌムを取埗したす。







芁確認各関数オブゞェクトには、スコヌプオブゞェクトぞの参照が含たれおいたす。 この䟋では、myCounter1.incrementずmyCounter2.incrementは、同じコヌドず同じプロパティ倀名前、長さなど を含む関数オブゞェクトを参照しおいたすが、[[scope]]は異なるスコヌプを参照しおいたすオブゞェクト 。



芖芚化を簡単にするためにダむアグラムには個別の関数オブゞェクトはありたせんが、それらはただ存圚しおいたす。



䟋



 var a, b; a = myCounter1.get(); // a == 100 b = myCounter2.get(); // b == 200 myCounter1.increment(1); myCounter1.increment(2); myCounter2.increment(5); a = myCounter1.get(); // a == 103 b = myCounter2.get(); // b == 205
      
      





これがその仕組みです。 閉鎖の抂念は力です。



スコヌプオブゞェクトのチェヌンずこれ



奜むず奜たざるずにかかわらず、これはスコヌプチェヌンの䞀郚ではありたせん。 この倀は、関数呌び出しパタヌンに䟝存したす。 ぀たり、同じ関数を呌び出すこずができたすが、内郚ではこの倀が異なりたす。



呌び出しパタヌン


このテヌマに぀いおは別の蚘事を曞く䟡倀があるので、ここでは衚面的にトピックを説明したす。 4぀のパタヌンがありたす。 ここに



メ゜ッド呌び出しパタヌン

 "use strict"; var myObj = { myProp: 100, myFunc: function myFunc() { return this.myProp; } }; myObj.myFunc(); //--  100
      
      





呌び出しにピリオドたたは[添え字]が含たれおいる堎合、関数はメ゜ッドずしお呌び出されたす。 䞊蚘の䟋では、これはmyObjを指したす。



関数呌び出しパタヌン

 "use strict"; function myFunc() { return this; } myFunc(); //--  undefined
      
      





この堎合、この倀はコヌドがストリクトモヌドで実行されおいるかどうかによっお異なりたす。







䞊蚘の䟋では、厳栌モヌドであるため、myFuncは未定矩を返したす。



コンストラクタヌ呌び出しパタヌン

 "use strict"; function MyObj() { this.a = 'a'; this.b = 'b'; } var myObj = new MyObj();
      
      





関数がプレフィックスnewで呌び出されるず、JavaScriptは指定された関数のprototypeプロパティから継承する新しいオブゞェクトを蚭定したす。 そしお、この新しく䜜成されたオブゞェクトは、このように関数に枡されたす。



呌び出しパタヌンを適甚する



 "use strict"; function myFunc(myArg) { return this.myProp + " " + myArg; } var result = myFunc.apply( { myProp: "prop" }, [ "arg" ] ); //--  — "prop arg"
      
      





このような任意の倀を枡すこずができたす。 この䟋では、Function.prototype.applyがこれに䜿甚されたす。 その他のオプション







次の䟋では、䞻にメ゜ッド呌び出しパタヌンを䜿甚しおいたす。



ネストされた関数でこれを䜿甚する



 "use strict"; var myObj = { myProp: "outer-value", createInnerObj: function createInnerObj() { var hidden = "value-in-closure"; return { myProp: "inner-value", innerFunc: function innerFunc() { return "hidden: '" + hidden + "', myProp: '" + this.myProp + "'"; } }; } }; var myInnerObj = myObj.createInnerObj(); console.log( myInnerObj.innerFunc() );
      
      





出力hidden 'value-in-closure'、myProp 'inner-value'



myObj.createInnerObjが呌び出されるたでに、次の構造が取埗されたす。







そしお、myInnerObj.innerFuncが呌び出されるたでに-このように







myObj.createInnerObjのこれはmyObjを指したすが、myInnerObj.innerFuncのこれはmyInnerObjを指したす。䞡方の関数はメ゜ッドずしお呌び出されたす。 したがっお、innerFunc内のthis.myPropは、倖郚倀ではなく内郚倀を返したす。



innerFuncをだたしおmyPropを次のように䜿甚できたす。



 var myInnerObj = myObj.createInnerObj(); var fakeObject = { myProp: "fake-inner-value", innerFunc: myInnerObj.innerFunc }; console.log( fakeObject.innerFunc() );
      
      





出力hidden 'value-in-closure'、myProp 'fake-inner-value'







たたは、applyたたはcallで



 var myInnerObj = myObj.createInnerObj(); console.log( myInnerObj.innerFunc.call( { myProp: "fake-inner-value-2", } ) );
      
      





出力hidden 'value-in-closure'、myProp 'fake-inner-value-2'



ただし、内郚関数が実際にアクセスする必芁がある堎合があり、内郚関数の呌び出し方法に関係なく、倖郚関数で䜿甚できたす。 これを行うには、次のように、クロヌゞャヌ぀たり、珟圚のスコヌプオブゞェクトに目的の倀を具䜓的に保存する必芁がありたす。var self = this; そしお、これの代わりに内郚関数でselfを䜿甚したす。



 "use strict"; var myObj = { myProp: "outer-value", createInnerObj: function createInnerObj() { var self = this; var hidden = "value-in-closure"; return { myProp: "inner-value", innerFunc: function innerFunc() { return "hidden: '" + hidden + "', myProp: '" + self.myProp + "'"; } }; } }; var myInnerObj = myObj.createInnerObj(); console.log( myInnerObj.innerFunc() );
      
      





出力hidden 'value-in-closure'、myProp 'outer-value'



次のようになりたす。







これで、innerFuncはクロヌゞャヌにあるselfを介しおこの倖郚関数の倀にアクセスできるこずがわかりたす。



おわりに



これで、最初の段萜からこれらの質問に答えるこずができたす。



閉鎖ずは䜕ですか これは、関数オブゞェクトずスコヌプオブゞェクトの䞡方に関連付けられたオブゞェクトです。 実際、JavaScriptのすべおの関数はクロヌゞャヌです。スコヌプオブゞェクトなしで関数オブゞェクトぞの参照を持぀こずは䞍可胜です。



クロヌゞャヌはい぀䜜成されたすか JavaScriptのすべおの関数はクロヌゞャヌなので、答えは明らかです。関数が蚭定されるずクロヌゞャヌが蚭定されたす。 したがっお、関数が定矩されるずクロヌゞャヌが䜜成されたす。 ただし、クロヌゞャの䜜成ず新しいスコヌプオブゞェクトの䜜成の違いを理解する必芁がありたす。関数が定矩されるずクロヌゞャ関数+スコヌプオブゞェクトの珟圚のチェヌンぞの参照が䜜成されたすが、呌び出されるず新しいスコヌプオブゞェクトが䜜成されたすクロヌゞャスコヌプオブゞェクトのチェヌンを倉曎するために䜿甚されたす機胜。



クロヌゞャはい぀砎壊されたすか JavaScriptの他のオブゞェクトず同様に、ガベヌゞコレクタヌは、それに察する参照がなくなったずきにクロヌゞャヌを凊理したす。



All Articles