web-dev-qa-db-ja.com

(JSで)クロージャについて何が便利なのですか?

JSのコンテキストでクロージャーを理解するための私の探求の中で、なぜクロージャーを使用する必要があるのか​​と疑問に思っています。親関数が戻った後でも、内部関数が親関数の変数にアクセスできることの何が素晴らしいのですか?それらの使い方が分からないので、その質問を正しくしたかどうかさえわかりません。

誰かがJSの現実世界の例を示すことができますか?

編集:質問 JavaScriptでクロージャーが重要なのはなぜですか? は、私が理解したいことを十分に明確に答えていませんでした。したがって、これは重複とは見なしません。

すべての回答/リソースを確認し、承認されたものにマークを付けます。皆に感謝します!

4
Mark Bubel

Javascriptは本質的に非同期です。あなたのコードは待つことができません。代わりに、「これが発生したら、この関数を呼び出します」と言います。これは、Javascriptコードが関数でいっぱいであることを意味します。他の関数に渡されて呼び出され、後で返されることもある関数によって返されることもあります。これらの関数は、多くの場合、それらの定義スコープが利用できるいくつかのデータを使用したいが、その呼び出し元(ブラウザ)は利用しません。

function getSize(src, callback){
  var img = new Image();
  img.onload = function(event){
     console.log(event);
     callback(img.width, img.height);
  }
  img.src = src
}

現在、これは通常のコードのように見えます。クロージャースコープを使用している以外は! imgcallback両方img.onloadによってクローズされます。ここで、クロージャーがなかったとしましょう。他のオプションは何ですか?イベントオブジェクトでimgを見つける可能性がありますが、callbackは確実に見つかりません。 2種類の引数を取る関数が必要です。関数を定義するときに設定される引数と、関数を呼び出すときに設定される引数です。または、必要なすべてをシステムに伝えることができます。または、ネイティブのカレーメカニズム*が必要です。イベントに反応する必要があるからといって、外部コードにすべてのことを伝えたくないので、そのようなメカニズムが存在すると仮定しましょう。 ES5はbindを導入しました:

*カリー化とは、既存の関数に引数を事前にバインドして新しい関数を作成し、残りの引数として元の関数に引数を送信する手法です。その後、元の関数の戻り値が返されます

function getSize(src, callback){
  var img = new Image();
  img.onload = function(callback, img, event){
     callback(img.width, img.height);
  }.bind(null, callback, img); //not needed if you have closures
  img.src = src
}

bindの引数は何ですか? nullthisコンテキストを設定します。架空のcurryメソッドはそうしません。次に、callbackおよびimg。これらは、img.onloadの引数と同じ順序である必要があります。これは時々受け入れられますが、上り坂と下り坂の両方の方向にカレーする必要がある場合は受け入れられません。さらに重要なことに、3つの引数がある場合は許容されますが、30がある場合は許容されません。

おもしろいことに、グローバルスコープは技術的にはクロージャスコープでもあります。しかし、そうでなかったとしても、モジュールのユーティリティ関数をグローバルスコープで定義する必要はありません(名前空間の問題)。関数は変数に格納されます!

getSize = function(Image, src, callback){
  var img = new Image();
  img.onload = function(callback, img, event){
     callback(img.width, img.height);
  }.bind(null, callback, img); //not needed if you have closures
  img.src = src
}.bind(null, Image) //not needed if you have closures

ここで、何らかの理由で画像座標を変換したいとします。その変換はソースに依存しています。 transformsrcはどこに追加する必要がありますか?

getSize = function(Image, transform, src, callback){ //here
  var img = new Image();
  img.onload = function(transform, src, callback, img, event){ //here
     var coord = transform(img.width, img.height, src); //used here
     callback(coord.x, coord.y);
  }.bind(null, transform, src, callback, img); //here
  img.src = src;
}.bind(null, transform, Image); //here

バグを見つけられますか?はい、外部バインド引数の順序が間違っています。

グローバルオブジェクトのすべてのパラメーターを保持することで、これを回避できます。もちろん、呼び出し元のスコープを変更したくないので、いくつかの追加の引数を渡したいときはいつでも独自のスコープを定義します。いくつかの良い慣例があれば、これはかなりバグがないかもしれません:

var scope = {Image: Image, ...}

getSize = function(scope, src, callback){
  var img = new scope.Image();
  var scope = {scope: scope, src: src, callback: callback, img:img};
  img.onload = function(scope, event){
     var coord = scope.transform(scope.img.width, scope.img.height, scope.src);
     callback(coord.x, coord.y);
  }.bind(null, scope);
  img.src = src;
}.bind(null, scope)

今すぐバグを見つけられますか? scope.transformundefinedです。 scope.scope.transformもう1つ必要です。あなたもそれを見つけることができますか?ここでscopeに1文字の変数を使用しても、これはばかげています。多分私達はプロトタイプチェーンを利用できます:

var scope = {Image: Image, ...}

getSize = function(scope, src, callback){
  var img = new scope.Image();
  var scope = scope.Object.create(scope)
  scope.src = src;
  scope.callback = callback;
  scope.img = img;
  img.onload = function(scope, event){
     var coord = scope.transform(scope.img.width, scope.img.height, scope.src);
     callback(coord.x, coord.y);
  }.bind(null, scope);
  img.src = src;
}.bind(null, scope)

もちろん、ローカル変数をスコープ内で直接定義して、内部関数がローカル変数を使用する可能性がある場合に2回宣言する必要がないようにすることもできます。残念ながら、そのようなトリックはありません。 arguments変数がありますが、それを私たちの目的に使用するのは少し面倒です(scope.getSizeArgs[0];クイック-どれですか?)。ただし、これまでのところ、閉鎖の欠如を回避するために:

  1. スコープごとに、親のスコープをプロトタイプとして、特別なscope変数を作成しました。
  2. それにすべての引数を追加しました
  3. 内部関数で必要になる可能性のあるすべてのローカル変数にそれを使用しました。

もちろん、これはばかげています。おそらく、宣言されていない変数を使用する場合、定義スコープで検索し、必要に応じてそのようなscopeオブジェクトを作成して、変数が定義関数より長く存続できるようにすることを言語が何らかの形で知っている場合はどうでしょうか。そうですね。

function getSize(src, callback){
  var img = new Image();
  img.onload = function(event);
     console.log(event);
     var coords = transform(img.width, img.height);
     callback(coords.x, coords.y);
  }
  img.src = src;
}

また、この明示的な受け渡しには別の欠点もあります。スコープに書き込むときは常に、常にローカル変数に書き込みます。内部関数から変数を変更することはできません。もちろん、1要素の配列または特別なReference<T>型を使用することもできますが、これは素晴らしい言語のにおいがしません。クロージャーを使用すると、これを直接行うことができます。これは本当に必要ですか?

var width = window.clientWidth;
window.addEventListener('resize', function(){width = window.clientWidth});
setInterval(function(){
  ...

代わりに30の引数を持つことが、呼び出し元のスコープを必要とする場合があると言うかもしれません。しかし、呼び出し元はyoがローカルスコープを表示することを望んでおらず、ある日重要と見なされる可能性がある変数名を回避する必要はありません。オブジェクトを渡すことはできますが、それらをスコープとは呼びません。 「データ転送オブジェクト」と言う人もいます。 「名前付き引数」という言葉を好む

$.ajax({
  url: "http://www.example.com/cors-api/echo.json",
  dataType: "json",
  success: function(response){
    ..

ここで、クロージャスコープの古典的な例として、静的変数nextIdのスコープを制限するために即座に呼び出される関数式を完成させます。

var getId = (function(){
  var nextId = 0;
  return function getId(){ //named for clarity
    return nextId++;
  }
})()

getId() //0
getId() //1
getId() //2
16
John Dvorak

クロージャについて本当に便利なのは、関数のシグネチャとは関係なく、コールバックに任意のデータを貼り付けることができることです。

便利な実例はアニメーションです。 JSは私の最強の言語ではないので、このためのコードを書くことすらしませんが、一般的な考え方は次のとおりです。

インターバルとコールバックの2つのパラメーターを取るTimerという関数があるとします。 (DOMには次のようなものがあり、JavaScriptでアニメーションを実装するために使用されることを知っています。)そして、インターバルが経過した後、コールバックを次のように呼び出します:CallbackFunction();

明らかな問題を参照してください?コールバック関数内で必要になる可能性のあるデータは返されません。一部のコールバックシステムでは、設定時に任意のオブジェクトを渡すことができ、コールバックが呼び出されると、そのオブジェクトがコンテキストとして渡されます。しかし、必要な場合、物事は面倒になります2コールバック内のデータの一部など.

解決策は、クロージャを渡し、必要なすべてのデータの周りにクロージャを置くことです。その後、後で(たとえば、タイマーが実行された後など)コールバックとして呼び出すことができ、作業を実行するために必要なすべてのデータ(囲まれた変数)を保持できます。

5
Mason Wheeler

閉鎖のシーンを1つ挙げます。

 function f1() {
      var n=999;

      setter=function() { n += 1 }
      function getter() {
           alert(n);
      }

      return f2;
 }

この関数では、変数nprivateになり、ゲッターメソッドとセッターメソッド(Javaなど)があります。

0
gravityplusplus

この例は、gravityplusplusによく似ています。 MasonとJanが指摘したように、非同期コードが最も一般的なケースです。しかし、ローカライズされたモジュール(「独自のタスクを実行するコードのブロック」を意味するモジュール)にも役立ちます。これは、すべて同じスコープ内にあるコードです(つまり、これを行う方法ではありません)

_counter = 0;

function incrementCounter() {
    counter += 1;
    return counter;
}

// Define Ryu's attack functions here
function lightPunch() {
    //TODO
}
function counter() {
    //TODO
}

function getCounterValue() {
    return counter;
}
_

getCounterValue()は何を返しますか?よくわかりません。数ではなく、私が宣言した関数になると思います。これらが何らかの方法でクロージャーに分離された場合(およびその方法がいくつかあり、重力が1つの良い例を挙げている)、変数名が混同される可能性はありません。そして、50以上のJSファイルで構成されるページを含む大規模な企業プロジェクトでは、変数名がたくさんあります。

0
Katana314