web-dev-qa-db-ja.com

関数のスコープが適切に維持されるようにJavaScriptコールバックを構造化する方法

XMLHttpRequestを使用していて、successコールバック関数のローカル変数にアクセスしたい。

これがコードです:

_function getFileContents(filePath, callbackFn) {  
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            callbackFn(xhr.responseText);
        }
    }
    xhr.open("GET", chrome.extension.getURL(filePath), true);
    xhr.send();
}
_

そして、私はそれをこのように呼びたいです:

_var test = "lol";

getFileContents("hello.js", function(data) {
    alert(test);
});
_

ここで、testはコールバック関数のスコープ外になります。なぜなら、コールバック関数内でアクセスできるのは、囲んでいる関数の変数のみだからです。 alert(test);testを正しく表示するようにtestをコールバック関数に渡すための最良の方法は何ですか?

編集:

ここで、上で定義した関数を呼び出す次のコードがあるとします。

_for (var test in testers) {
    getFileContents("hello.js", function(data) {
        alert(test);
    });
}
_

alert(test);コードは、testループからのforの最後の値のみを出力します。関数testが呼び出されたときにgetFileContentsの値を出力するようにするにはどうすればよいですか? (getFileContentsを変更せずにこれを実行したいと思います。これは非常に一般的なヘルパー関数であり、testのような特定の変数を渡して特定にしたくないためです。

33
Chetan

あなたが提供したコードでtestはまだコールバック内のスコープにあります。 xhrは、dataとして渡されるxhr.responseText以外は使用されません。

コメントから更新

コードが次のようになっていると仮定します。

for (var test in testers)
  getFileContents("hello"+test+".js", function(data) {
    alert(test);
  });
}

このスクリプトを実行すると、testtestersのキーの値が割り当てられます-getFileContentsが呼び出されるたびに、バックグラウンドでリクエストが開始されます。リクエストが完了すると、コールバックが呼び出されます。 testには、ループの実行がすでに終了しているため、ループからのFINAL VALUEが含まれます。

この種の問題を修正するクロージャーと呼ばれる使用可能な手法があります。コールバック関数を返す関数を作成し、次のようにして変数に保持できる新しいスコープを作成できます。

for (var test in testers) {
  getFileContents("hello"+test+".js", 
    (function(test) { // lets create a function who has a single argument "test"
      // inside this function test will refer to the functions argument
      return function(data) {
        // test still refers to the closure functions argument
        alert(test);
      };
    })(test) // immediately call the closure with the current value of test
  );
}

これにより、testの値を「保持」する新しいスコープが(新しい関数と共に)基本的に作成されます。

同じようなことを書く別の方法:

for (var test in testers) {
  (function(test) { // lets create a function who has a single argument "test"
    // inside this function test will refer to the functions argument
    // not the var test from the loop above
    getFileContents("hello"+test+".js", function(data) {
        // test still refers to the closure functions argument
        alert(test);
    });
  })(test); // immediately call the closure with the value of `test` from `testers`
}
41
gnarf

JavaScriptは 字句スコープ を使用します。これは基本的に、2番目のコード例が意図したとおりに機能することを意味します。

David Flanaganの Definitive Guide から借りた次の例を考えてみましょう。1

var x = "global";

function f() {
  var x = "local";
  function g() { alert(x); }
  g();
}

f();  // Calling this function displays "local"

また、C、C++、Javaとは異なり、JavaScriptにはブロックレベルのスコープがないことに注意してください。

また、次の記事も参考にしてください。


1 David Flanagan: JavaScript-The Definitive Guide 、第4版、48ページ。

7
Daniel Vassallo

このシナリオでは、期待どおりにテストが解決されますが、thisの値は異なる場合があります。通常、スコープを保持するには、次のように非同期関数のパラメーターにします。

function getFileContents(filePath, callbackFn, scope) {  
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            callbackFn.call(scope, xhr.responseText);
        }
    }
    xhr.open("GET", chrome.extension.getURL(filePath), true);
    xhr.send();
}


//then to call it:
var test = "lol";

getFileContents("hello.js", function(data) {
    alert(test);
}, this);
2
Igor Zevaka

私は同様の問題に遭遇しました。私のコードは次のようになりました:

for (var i=0; i<textFilesObj.length; i++)
{
    var xmlHttp=new XMLHttpRequest();
    var name = "compile/" + textFilesObj[i].fileName;
    var content = textFilesObj[i].content;

    xmlHttp.onreadystatechange=function()
    {
        if(xmlHttp.readyState==4)
        {
            var responseText = xmlHttp.responseText;
            Debug(responseText);
        }
    }

    xmlHttp.open("POST","save1.php",true);
    xmlHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xmlHttp.send("filename="+name+"&text="+encodeURIComponent(content));
}

多くの場合、出力は最後のオブジェクトの応答テキストでした(ただし、常にではありません)。実際、それは一種のランダムであり、応答の数さえも変化しました(一定の入力に対して)。関数が作成されている必要があることがわかります。

    xmlHttp.onreadystatechange=function()
    {
        if(this.readyState==4)
        {
            var responseText = this.responseText;
            Debug(responseText);
        }
    }

基本的に、最初のバージョンでは、「xmlHttp」オブジェクトはループの現在のステージのバージョンに設定されます。ほとんどの場合、ループはAJAX要求のいずれかが完了する前に完了していたため、この場合、「xmlHttp」はループの最後のインスタンスを参照しました。この最後の要求が他の前に完了した場合次に、準備状態が変化したとき(準備状態が<4であっても)は、最後の要求からの応答をすべて出力します。

解決策は、「xmlHttp」を「this」に置き換えることです。これにより、コールバックが呼び出されるたびに、内部関数がリクエストオブジェクトの正しいインスタンスを参照します。

0
Bruce