JavaScriptクロージャーに関するDanAbramov

クロージャーは「見えない」構造であるため、プログラマーにとっては困難です。



オブジェクト、変数、または関数を使用するときは、意図的に使用します。 「これが変数が必要な場所です」と考え、それをコードに追加します。 ただし、閉鎖は別のものです。ほとんどのプログラマーはクロージャーについて学び始めていますが、これらの人々はすでにクロージャーを知らずに使用しています。おそらく同じことがあなたにも起こります。したがって、クロージャを学ぶことは、以前に何度も遭遇したことを認識する方法を学ぶことよりも、新しいアイデアを学ぶことではありません。 一言で言えば、クロージャとは、関数がその外部で宣言された変数にアクセスすることです。たとえば、クロージャーは次のコードに含まれています。















let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


これuser => user.startsWith(query)は関数であることに注意してください彼女は変数を使用しますqueryそして、この変数は関数の外部で宣言されています。これは閉鎖です。



必要に応じて、読むことをスキップできます。この資料の残りの部分では、クロージャーを別の観点から見ていきます。クロージャーとは何かについて話すのではなく、記事のこの部分では、クロージャーの検出の詳細について説明します。これは、最初のプログラマーが1960年代に働いた方法と似ています。



ステップ1:関数は、関数の外部で宣言された変数にアクセスできます



クロージャを理解するには、変数と関数にかなり精通している必要があります。この例ではfood、関数内で変数を宣言していますeat



function eat() {
  let food = 'cheese';
  console.log(food + ' is good');
}
eat(); //    'cheese is good'


food関数の外部で、 後で変数の値を変更できるようにしたい場合はどうなりますeatか?これを行うには、変数自体を関数から削除foodして、より高いレベルに移動します。



let food = 'cheese'; //     
function eat() {
  console.log(food + ' is good');
}


これによりfood、必要に応じて「外部から」変数を変更できます



eat(); //  'cheese is good'
food = 'pizza';
eat(); //  'pizza is good'
food = 'sushi';
eat(); //  'sushi is good'


言い換えると、変数はfood関数に対してeatローカルでなくなりますしかし、eatこの関数は、これにもかかわらず、この変数の操作に問題はありません。関数は、関数の外部で宣言された変数にアクセスできます。しばらく立ち止まって自分自身をチェックし、このアイデアに問題がないことを確認してください。この考えがしっかりと頭に浮かんだら、2番目のステップに進みます。



ステップ2:関数呼び出しにコードを配置する



いくつかのコードがあるとしましょう:



/*   */


それがどのコードであるかは関係ありません。しかし、2回実行する必要があるとしましょう。



これを行う最初の方法は、コードのコピーを作成することです。



/*   */
/*   */


別の方法は、コードをループに入れることです。



for (let i = 0; i < 2; i++) {
  /*   */
}


そして、今日私たちにとって特に興味深い3番目の方法は、このコードを関数に入れることです。



function doTheThing() {
  /*   */
}
doTheThing();
doTheThing();


関数を使用すると、プログラム内のどこからでも、いつでも、指定されたコードを何度でも呼び出すことができるため、最大限の柔軟性が得られます。



実際、必要に応じて、新しい関数の呼び出しを1回だけに制限できます。



function doTheThing() {
  /*   */
}
doTheThing();


上記のコードは、元のコードスニペットと同等であることに注意してください。



/*   */


つまり、コードの一部を取得して関数に「ラップ」し、この関数を1回だけ呼び出すと、このコードの動作に影響を与えることはありません。この規則にはいくつかの例外があり、注意を払うことはありませんが、一般に、この規則は正しいと見なすことができます。しばらく考えて、この考えに慣れてください。



ステップ3:クロージャーを検出する



私たちは2つのアイデアを考え出しました。



  • 関数は、関数の外部で宣言された変数を処理できます。
  • コードを関数に配置し、この関数を1回呼び出すと、コードの結果には影響しません。


次に、これら2つのアイデアを組み合わせるとどうなるかについて説明しましょう。



最初のステップで見たサンプルコードを見てみましょう。



let food = 'cheese';
function eat() {
  console.log(food + ' is good');
}
eat();


ここで、この例全体を、一度だけ呼び出す予定の関数に入れましょう。



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  eat();
}
liveADay();


前のコード例の両方を読んで、それらが同等であることを確認してください。



2番目の例は機能します!しかし、それを詳しく見てみましょう。関数eat関数内にあることに注意してくださいliveADayこれはJavaScriptで許可されていますか?ある関数を別の関数の中にラップすることは本当に可能ですか?



このように構造化されたコードが正しくないことが判明する言語があります。たとえば、Cでは、そのようなコードは正しくありません(この言語にはクロージャーはありません)。これは、Cを使用する場合、2番目の結論が正しくないことを意味します。任意のコードを取得して関数に「ラップ」することはできません。しかし、JavaScriptにはそのような制限はありません。



変数が宣言されている場所と使用されている場所に特に注意して、このコードについてもう一度考えてみましょう。food



function liveADay() {
  let food = 'cheese'; //  `food`
  function eat() {
    console.log(food + ' is good'); //   `food`
  }
  eat();
}
liveADay();


このコードを段階的に一緒に見ていきましょう。まず、トップレベルで関数を宣言しますliveADay。すぐに彼女に電話します。この関数にはローカル変数がありますfood。関数もその中で宣言されていeatます。次に、liveADay関数は内部的に呼び出されeatます。関数があるのでeat、関数内でliveADay、それがeatで宣言されたすべての変数を「見ます」liveADay。これが、関数eatが変数の値を読み取ることができる理由ですfood



これはクロージャと呼ばれます。



関数(などeat)が変数(などfoodの値を読み書きするときのクロージャの存在について説明します。変数の値は、変数の外部(たとえば、関数内liveADayで宣言されます。



これらの言葉について考え、読み直してください。サンプルコードで私たちが話していることを見つけて、自分自身をテストしてください。



これは、記事の冒頭で示した例です。



let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


関数式を使用してこの例を書き直すと、クロージャに気付くのが簡単になる場合があります。



let users = ['Alice', 'Dan', 'Jessica'];
// 1.  query    
let query = 'A';
let user = users.filter(function(user) {
  // 2.     
  // 3.      query (    !)
  return user.startsWith(query);
});


関数がその外部で宣言された変数にアクセスするとき、それをクロージャーと呼びます。用語自体は十分に大まかに使用されます。一部の人々は、例に示されているように、ネストされた関数自体を「クロージャー」と呼びます。他の人は、それを「クロージャー」と呼ぶことによって外部変数アクセサーを参照するかもしれません。実際には、これは問題ではありません。



関数呼び出しゴースト



閉鎖は、今あなたには一見単純な概念のように思えるかもしれません。しかし、これは、それらがいくつかの非自明な機能を欠いていることを意味するものではありません。関数がその外部の変数の値を読み書きできるという事実を注意深く考えると、これはかなり深刻な結果をもたらすことがわかります。



たとえば、これは、別の関数内にネストされた関数を呼び出すことができる限り、そのような変数が「存続」することを意味します。



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  //  eat   
  setTimeout(eat, 5000);
}
liveADay();


この例でfood、これは関数呼び出し内のローカル変数ですliveADay()。関数を終了した後、この変数が「消える」こと、そして幽霊のように私たちを悩ませることに戻らないことを決定したいだけです。



しかし、関数でliveADayeat、5秒後に関数を呼び出すようにブラウザに要求します。そして、この関数は変数の値を読み取りますfood。その結果、JavaScriptエンジンは、関数が呼び出されるまで、foodこの呼び出しに関連付けられた変数を存続させる必要があることがわかりました この意味で、クロージャは、過去の関数呼び出しの「ゴースト」、またはそのような呼び出しの「メモリ」と考えることができます。関数の実行にもかかわらずliveADay()eat



liveADay()ずっと前に終了しましたが、その中で宣言された変数は、ネストされた関数を呼び出すeatことができる限り存在し続ける必要があります。幸い、JavaScriptがこれらのメカニズムを処理するため、これらの状況で特別なことを行う必要はありません。



なぜ「閉鎖」はそのように呼ばれるのですか?



なぜクロージャーがそのように呼ばれるのか不思議に思うかもしれません。この理由は主に歴史的なものです。コンピュータの専門用語に精通している人なら誰でも、このような表現user => user.startsWith(query)には「オープンバインディング」があると言うかもしれません。つまり、この式からuser(パラメータ)が何であるかは明らかですが、単独で見ると、それが何であるかは明確ではありませんquery。実際、query関数の外部で宣言されているのは変数であると言うとき、そのオープンバインディングを「閉じている」のです。言い換えれば、私たちは閉鎖を取得します。



クロージャは、すべてのプログラミング言語で実装されているわけではありません。たとえば、Cなどの一部の言語では、ネストされた関数をまったく使用できません。その結果、関数はローカル変数またはグローバル変数でのみ機能します。ただし、親関数のローカル変数にアクセスできる状況はありません。これは実際には非常に不快な制限です。



クロージャーを実装するRustのような言語もあります。ただし、クロージャと通常の機能を説明するために異なる構文を使用します。その結果、関数の外部で変数の値を読み取る必要がある場合は、Rustを使用して特別な構造を使用する必要があります。これは、クロージャを使用すると、関数呼び出しが完了した後でも、外部変数(「環境」と呼ばれる)を格納するための言語の内部メカニズムが必要になる場合があるためです。システムへのこの追加の負荷はJavaScriptで許容できますが、かなり低レベルの言語で使用すると、パフォーマンスの問題が発生する可能性があります。



ここで、JavaScriptのクロージャーの概念を理解していただければ幸いです。



JavaScriptの概念を理解するのに苦労していますか?






All Articles