初心者向けに複雑なことをJavaScriptで説明しようとする

Javascriptのクロージャーがどのように機能するか、これがどのように機能するか、クラスのコンストラクターを作成する方法、およびそれらを作成するためのさまざまなアプローチがどのように異なるかを簡単に説明しようとします。

この記事は革新的であると主張しているわけではありませんが、初心者にとってどのように機能するかについての十分にアクセス可能な説明を見たことはありません。



短絡


ウィキペディアによると、クロージャーは他の関数で定義された関数です。

javascriptのクロージャーは、暗黙的に「メイン関数」の本体にあるため、すべて関数です。

彼らはどんな人ですか? 「閉鎖」とはどういう意味ですか?

用語の意味は非常に単純で、簡単に説明することもできます。

ロック関数には、関数自体のコンテキストだけでなく、上記のすべてのレベルで作成された変数にアクセスする機能があります。

コードを説明します。



var a = 1; var b = 2; function closureFirstLevel() { return a + b; } function createrOfSecondLevelClosure() { var c = a + b; return function() { return c + closureFirstLevel() + a + b; } } var c = createrOfSecondLevelClosure(); function runCInAnotherContext() { return c(); } console.log(a,b); console.log('  a & b       :',closureFirstLevel()); console.log('  c (   ),        ,   a  b       :',c());
      
      





何かが理解できなくなった場合、少し理解してみましょう。

closureFirstLevelは、この関数の外部で宣言された変数(外部変数)を参照し、その合計を返します。

createrOfSecondLevelClosureは変数aおよびbにアクセスし、この関数で宣言された変数にそれらの合計を格納し、c、closureFirstLevelによって返された結果、2レベル下で宣言された変数aおよびbを考慮する関数を返します。



runCInAnotherContextを実行すると、関数 'c'が実行されます(結局、createrOfSecondLevelClosureは保存可能な関数を返し、変数 'c​​'はグローバルスコープで宣言され、この関数を書き込みます)。 runCInAnotherContext関数は、初期化中にこれらの変数にロックされたため、コンテキスト外で宣言されました。



大量イベント作成のクロージャー。

ループとして使用される変数への参照は、ループの実行中は常に参照として渡されます(通常は数値ですが)。 その結果、作成されたすべての関数はこの変数の最後の値を持ちます。

例を参照



 var elem = document.getElementsByTagName('a'); for (var n = 0, l = elem.length; n < l; n++ ) { elem[n].onclick = function() { alert(n); return false; } } //         alert
      
      





関数を閉じることができます:



 var elem = document.getElementsByTagName('a'); for (var n = 0, l = elem.length; n < l; n++ ) { elem[n].onclick = function(x) { return function() { alert(x); return false; } }(n); // ,            alert   click  . }
      
      





また、まったく異なるアプローチを使用することもできます。 配列の場合、forEachメソッド(EcmaScript5標準の一部)は、forループのようには機能しません。

引数を1つ取ります-要素を処理し、引数をとる関数:elementOfArray、positionInArray、Array。 そして、当然、この関数はそのコンテキストで呼び出されます。

どこかで、最初の引数だけを受け入れれば十分です。

実行コンテキストを変更することにより、NodeListオブジェクトに対してこの関数を呼び出すことができます。 (これがどのように機能するかについてのより完全な説明については、これおよびプロトタイプに関する記事の一部を参照してください)。



 var elem = document.getElementsByTagName('a'); Array.prototype.forEach.call(elem,function(el,position) { el.onclick = function() { alert(position); return false; } })
      
      







このキーワード


この単語は、関数を呼び出す現在のオブジェクトを指します。

グローバルコンテキストで宣言されたすべての関数は、ウィンドウオブジェクトのメソッド(ブラウザ内)であり、このコンテキストでコンテキストなしで呼び出されるすべての関数はウィンドウを参照します。

非同期プログラミングを扱うようになるまで、すべては非常に簡単です。



 var a = { property1: 1, property2: 2, func: function() { console.log(this.property1 + this.property2, 'test'); return this.property1 + this.property2; } } console.log(a.func()); //this    'a',    . setTimeout(function() { console.log(a.func()); //this       'a',   ,   ,     'a' },100); setTimeout(a.func,101); //   , NaN (   undefined + undefined) //,      ,          ,   
      
      





setTimeoutの代わりに、setIntervalまたはイベントハンドラーのバインド(たとえば、elem.onclickまたはaddEventListener)、または遅延計算を実行する他の方法に置き換えることができます。いずれも何らかの方法で実行コンテキストが失われます。 これを保存するには、いくつかの方法があります。

これを単純に匿名関数でラップし、var that = this variableを作成してこれの代わりに使用することができます(もちろん、呼び出された関数の外部に変数を作成します)。 このため、関数には組み込みのバインドメソッドがあり(EcmaScript 5標準で利用可能になったため、古いブラウザーのサポートを実装する必要があります)、目的のコンテキストと引数にバインドされた新しい関数を返します。 例:



 function logCurrentThisPlease() { console.log(this); } logCurrentThisPlease(); //window var a = {} a.logCurrentThisPlease = logCurrentThisPlease; a.logCurrentThisPlease(); //a setTimeout(a.logCurrentThisPlease, 100); //window,         setTimeout(function() { a.logCurrentThisPlease(); }, 200);//a setTimeout(function() { this.logCurrentThisPlease(); }.bind(a), 200);//a var that = a; function logCurrentThatPlease() { console.log(that); } logCurrentThatPlease(); //a setTimeout(logCurrentThatPlease, 200);//a var logCurrentBindedContextPlease = logCurrentThisPlease.bind(a); //  — ,    ,   —   logCurrentBindedContextPlease(); //a setTimeout(logCurrentBindedContextPlease, 200); //a
      
      





さて、もっと複雑な例です。

定期的な間隔で動作する再帰関数の損失。



 var a = { i: 0, infinityIncrementation: function() { console.log( this.i++ ); if (this.i < Infinity) setTimeout(this.infinityIncrementation,500); } } a.infinityIncrementation(); // 0,undefined —  ,      a.infinityIncrementation = a.infinityIncrementation.bind(a); //     a.infinityIncrementation(); //0,1,2,3,4,5,6,7,8,9,10...Infinity-1 //  var b = { i: 0, infinityIncrementation: function() { console.log( this.i++ ); if (this.i < Infinity) setTimeout(function() {this.infinityIncrementation}.bind(this),500); } } b.infinityIncrementation(); //0,1,2,3,4,5,6,7,8,9,10...Infinity-1
      
      





なぜ2番目の作業方法が正しく、最初の作業方法が正しくないのかについては、記事のプロトタイプ部分を参照してください。



実行コンテキストを変更できる関数メソッド-バインド、呼び出し、適用


Function.bindは、最初の引数を実行されるコンテキスト(これが何であるか)として、残りを返された関数が呼び出される無制限の数の引数として取るメソッドです。

Function.applyは関数を呼び出すメソッドであり、最初の引数は関数内でこれになる引数、2番目は関数が呼び出される引数の配列です。

Function.callはapplyと同じですが、2番目の引数の代わりに、無制限の数の引数が関数に渡されます。



オブジェクトコンストラクター


多くの場合、次のようなコンストラクターが作成されます。



 function SuperObjectConstructor() { this.a = 1; this.b = 2; this.summ = function() { return this.a + this.b; } }
      
      





そして、これはあまり正しくありません。 ここで何が間違っていますか? この例では、コンストラクター本体で関数が宣言されているという点が1つだけ間違っています。 なぜこれが悪いのですか?

まず、関数のプロトタイプを変更してそのような関数をオーバーライドしても機能しません。つまり、このコンストラクターで初期化されたすべてのオブジェクトはメソッドを別のものに変更できません。 通常、継承する機能はなくなります。

第二に-過剰なメモリ消費:



 var a = new SuperObjectConstructor(); var b = new SuperObjectConstructor(); console.log(a.summ == b.summ); //false
      
      





関数が再作成されるたびに。

デザイナーの良いトーン(およびコードのより良い理解)によって、それだけに固有の変数(より正確には、オブジェクトのフィールド)のみを定義する必要があります。

残りの部分はプロトタイプを通じてより適切に定義されます。いずれの場合でも、一般的なプロパティまたはメソッドを再定義する必要がある特定のオブジェクトについてのみ、これはプロトタイプに影響を与えることなく直接実行できます。

方法:



 function SuperObjectConstructorRightVersion(a,b) { this.a = a || this.constructor.prototype.a; //      this.b = b || this.constructor.prototype.b; } SuperObjectConstructorRightVersion.prototype = { //   constructor: SuperObjectConstructorRightVersion, //          a: 1, b: 2, summ: function() { return this.a + this.b; } } /*   SuperObjectConstructorRightVersion.prototype.a = 1; SuperObjectConstructorRightVersion.prototype.b = 2; SuperObjectConstructorRightVersion.prototype.summ = function() {....};        . */ var abc = new SuperObjectConstructorRightVersion(); console.log(abc.summ());//3 var bfg = new SuperObjectConstructorRightVersion(5,20); console.log(bfg.summ());//25
      
      





多くのjavascriptには、オブジェクトとそのメソッドのみが直接アクセスできるプライベートメソッドや関数など、ほとんど常に自己監視にのみ必要な機能が欠けており、関数の本体で宣言された変数や関数として実装されることがよくあります。コンストラクター。 多くの人はこれが悪い調子だと言いますが、これが悪い調子である理由を言う人はほとんどいません。

その理由は1つです。なぜなら、このコンストラクターで何かを変更する必要がある場合は、プロトタイプを使用するのではなく、ソースに移動してそこを変更する必要があるからです。

また、これらの「プライベート」プロパティとメソッドの使用を拡大するために、このコンストラクターから継承することは非常に困難です。



もう一つの微妙な点。 このメソッドを他のオブジェクトに転送したり、別のコンテキストで使用したりする場合は、bindを使用してメソッドをオブジェクトのコンテキストにバインドしないでください(初期化中のコンストラクターで、基本的には終了できます)。

これにより、埋め込みオブジェクトを実行できます。

たとえば、他の列挙可能な(列挙された)オブジェクトに対してforEach配列メソッドを使用できます。 たとえば、すべてのタイプのNodeList(生きているものと生きていないもの)について(上記を参照)。



おわりに


例として、記事の内容を組み合わせて、大きなコンストラクターを作成しませんか。



 function Monster(name, hp, dmg) { this.name = name || this.constructor.prototype.name(); this.hp = hp || this.constructor.prototype.hp; this.dmg = dmg || this.constructor.prototype.dmg; } Monster.prototype = { constructor: Monster, hp: 10, dmg: 3, name: function() { return 'RandomMonster'+(new Date).getTime(); }, offerFight: function(enemy) { if (!enemy.acceptFight) { alert('this thing cant fight with me :('); return; } enemy.acceptFight(this); this.acceptFight(enemy); }, acceptFight: function(enemy) { var timeout = 50 + this.diceRollForRandom(); this.attack(enemy,timeout); }, diceRollForRandom: function() { return (Math.random() >= 0.5 ? 50 : 20); }, takeDmg: function(dmg) { console.log(this.name,' was damaged (',dmg,'),current HP is ',this.hp-dmg); return this.hp -= dmg; }, attack: function(enemy,timeout) { if (enemy.takeDmg(this.dmg) <= 0) { enemy.die(); this.win(); return; } this.to = setTimeout(function() {this.attack(enemy)}.bind(this),timeout); }, win: function() { alert('My name is ' + this.name + ', and Im a winner'); }, die: function() { alert('I died, ' + this.name); clearTimeout(this.to); } } var ChuckNorris = new Monster('Chuck Norris', 100, 100); var MikhailBoyarsky = new Monster('Misha Boyarsky', 200, 50); MikhailBoyarsky.offerFight(ChuckNorris);
      
      





このとんでもない例では、原則として、すべてがあります:呼び出しコンテキストの保存、クロージャー、コンストラクターの作成。

批判と訂正を希望します(急いで何かを追加するのを忘れるかもしれませんが、間違っているだけです)。

ps boyarsは時々勝ちます。




All Articles