関数内で変数を変更した後、変数が変更されないのはなぜですか? - 非同期コードリファレンス 質問する

関数内で変数を変更した後、変数が変更されないのはなぜですか? - 非同期コードリファレンス 質問する

次の例を見ると、outerScopeVarすべてのケースで undefined になるのはなぜでしょうか?

var outerScopeVar;

var img = document.createElement('img');
img.onload = function() {
    outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
    outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
    outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
    outerScopeVar = response;
});
console.log(outerScopeVar);
// with observables
var outerScopeVar;
myObservable.subscribe(function (value) {
    outerScopeVar = value;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

これらすべての例でなぜ出力されるのでしょうかundefined? 回避策は必要なく、なぜこれが起こるのかを知りたいのです。


注:これはJavaScript の非同期性に関する標準的な質問です。この質問を自由に改善し、コミュニティが理解できるより単純な例を追加してください。

ベストアンサー1

一言で答えると、非同期性です

序文

このトピックは、Stack Overflow で少なくとも数千回繰り返されています。そこで、まず最初に、非常に役立つリソースをいくつか紹介したいと思います。


目の前の質問に対する答え

まず、共通の動作をたどってみましょう。すべての例で、 は関数outerScopeVar内で変更されます。その関数は明らかにすぐには実行されず、引数として割り当てられるか渡されます。これをコールバックと呼びます。

ここで問題になるのは、そのコールバックがいつ呼び出されるかということです。

それはケースによって異なります。もう一度、いくつかの一般的な動作をトレースしてみましょう。

  • img.onload画像が正常に読み込まれたときに、将来呼び出される可能性があります。
  • setTimeout遅延が経過し、タイムアウトが によってキャンセルされていない場合、将来的に呼び出される可能性がありますclearTimeout。注: 遅延として使用する場合でも0、すべてのブラウザには最小のタイムアウト遅延キャップがあります (HTML5 仕様では 4 ミリ秒に指定されています)。
  • jQuery$.postのコールバックは、Ajax リクエストが正常に完了したときに、将来呼び出される可能性があります。
  • Node.js は、ファイルが正常に読み取られたとき、またはエラーがスローされたときに、将来fs.readFile呼び出される可能性があります。

いずれの場合も、将来実行される可能性のあるコールバックがあります。この「将来実行される可能性のある」を、非同期フローと呼びます。

非同期実行は同期フローから押し出されます。つまり、同期コード スタックの実行中は、非同期コードは実行されませんこれが、JavaScript がシングル スレッドであることを意味します。

具体的には、JSエンジンがアイドル状態(非同期コードのスタックを実行していない状態)のときに、非同期コールバックをトリガーした可能性のあるイベント(タイムアウトの期限切れ、ネットワーク応答の受信など)をポーリングし、それらを次々に実行します。これは、イベントループ

つまり、手描きの赤い図形で強調表示された非同期コードは、それぞれのコード ブロック内の残りの同期コードがすべて実行された後にのみ実行される可能性があります。

非同期コードが強調表示されました

つまり、コールバック関数は同期的に作成されますが、非同期的に実行されます。非同期関数が実行されたことが分かるまで、その実行を信頼することはできません。では、どうすればよいでしょうか。

実に簡単です。非同期関数の実行に依存するロジックは、この非同期関数内から開始/呼び出される必要があります。たとえば、コールバック関数内でalertとを移動するconsole.logと、その時点で結果が利用可能になるため、期待どおりの結果が出力されます。

独自のコールバックロジックを実装する

多くの場合、非同期関数の結果を使用してより多くの処理を実行したり、非同期関数が呼び出された場所に応じて結果を使用して異なる処理を実行したりする必要があります。もう少し複雑な例に取り組んでみましょう。

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

注:一般的な非同期関数としてランダム遅延を使用していますsetTimeout。同じ例が Ajax、、、およびその他の非同期フローにも当てはまりreadFileますonload

この例は明らかに他の例と同じ問題を抱えています。つまり、非同期関数が実行されるまで待機しないのです。

独自のコールバック システムを実装して、これに取り組みましょう。まず、outerScopeVarこの場合はまったく役に立たない醜い部分を取り除きます。次に、関数の引数を受け入れるパラメーター (コールバック) を追加します。非同期操作が終了したら、このコールバックを呼び出して結果を渡します。実装 (コメントを順番に読んでください) は次のとおりです。

// 1. Call helloCatAsync passing a callback function,
//    which will be called receiving the result from the async operation
helloCatAsync(function(result) {
    // 5. Received the result from the async function,
    //    now do whatever you want with it:
    alert(result);
});

// 2. The "callback" parameter is a reference to the function which
//    was passed as an argument from the helloCatAsync call
function helloCatAsync(callback) {
    // 3. Start async operation:
    setTimeout(function() {
        // 4. Finished async operation,
        //    call the callback, passing the result as an argument
        callback('Nya');
    }, Math.random() * 2000);
}

上記の例のコードスニペット:

// 1. Call helloCatAsync passing a callback function,
//    which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
    // 5. Received the result from the async function,
    //    now do whatever you want with it:
    console.log("5. result is: ", result);
});

// 2. The "callback" parameter is a reference to the function which
//    was passed as an argument from the helloCatAsync call
function helloCatAsync(callback) {
    console.log("2. callback here is the function passed as argument above...")
    // 3. Start async operation:
    setTimeout(function() {
    console.log("3. start async operation...")
    console.log("4. finished async operation, calling the callback, passing the result...")
        // 4. Finished async operation,
        //    call the callback passing the result as argument
        callback('Nya');
    }, Math.random() * 2000);
}

実際の使用例では、DOM API とほとんどのライブラリが既にコールバック機能 (helloCatAsyncこのデモンストレーション例の実装) を提供しています。コールバック関数を渡し、それが同期フロー外で実行されることを理解し、それに対応するようにコードを再構築するだけで済みます。

また、非同期の性質上、return非同期コールバックは同期コードの実行が終了してからかなり経ってから実行されるため、非同期フローからコールバックが定義された同期フローに値を戻すことは不可能であることにも気づくでしょう。

return非同期コールバックから値を取得する代わりに、コールバック パターン、つまり Promise を使用する必要があります。

約束

維持する方法はありますが、コールバック地獄バニラJSではプロミスが主流でしたが、プロミスは人気が高まり、現在ES6で標準化されています(約束 - MDN)。

Promise (別名 Futures) は、非同期コードのより直線的で読みやすいものを提供しますが、その機能全体を説明することはこの質問の範囲外です。代わりに、興味のある人のために、次の優れたリソースを残しておきます。


JavaScript の非同期性に関するその他の参考資料


注:この回答はコミュニティ Wiki としてマークしました。したがって、少なくとも 100 の評価を持つ人なら誰でも編集して改善できます。この回答を改善したり、まったく新しい回答を送信したりしてください。

私はこの質問を、Ajaxとは関係のない非同期性の問題について答えるための標準的なトピックにしたいと思っています(AJAX 呼び出しからの応答を返すにはどうすればいいですか?そのため、このトピックをできるだけ良くし、役立つものにするためには、あなたの助けが必要です。

おすすめ記事