web-dev-qa-db-ja.com

関数内で変数を変更しても変数が変更されないのはなぜですか? - 非同期コード参照

次の例で、どうしてouterScopeVarが未定義になっているのでしょうか。

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);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
    outerScopeVar = pos;
});
console.log(outerScopeVar);

これらすべての例でなぜundefinedが出力されるのですか?私は回避策を望みません、私は知りたいです なぜ これは起こっています。


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

638

1つの答え:非同期性

序文

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


手元の質問に対する答え

最初に一般的な動作をトレースしましょう。すべての例で、outerScopeVarfunction内で変更されます。その関数は明らかにすぐには実行されず、引数として割り当てられているか、渡されています。それが、callbackと呼ばれるものです。

質問は、そのコールバックがいつ呼び出されるかです。

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

  • img.onloadは、イメージが正常にロードされたとき(およびロードされた場合)に将来のある時期と呼ばれる場合があります。
  • setTimeoutは、遅延が期限切れになり、タイムアウトがclearTimeoutによってキャンセルされなかった後、将来のある時点と呼ばれる場合があります。注:0を遅延として使用する場合でも、すべてのブラウザーには最小タイムアウト遅延キャップがあります(HTML5仕様で4msに指定されています)。
  • jQuery $.postのコールバックは、Ajaxリクエストが正常に完了した場合(および完了した場合)に将来のある時点と呼ばれる場合があります。
  • Node.jsのfs.readFileは、ファイルが正常に読み取られたとき、またはエラーがスローされたときに将来的にはと呼ばれる場合があります。

すべての場合において、将来のある時点を実行できるコールバックがあります。この「将来のある時点」は、非同期フローと呼ばれるものです。

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

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

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

async code highlighted

つまり、コールバック関数は同期的に作成されますが、非同期的に実行されます。非同期関数が実行されたことを知るまで、非同期関数の実行に依存することはできません。その方法を教えてください。

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

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

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

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

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

注:ランダムな遅延でsetTimeoutを一般的な非同期関数として使用しています。同じ例がAjaxに適用されます、readFileonloadおよびその他の非同期フロー。

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

独自のコールバックシステムの実装に取り​​組みましょう。最初に、このthisい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 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 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 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にすることは不可能であることに注意してください。実行が終了しました。

非同期コールバックから値をreturningする代わりに、コールバックパターンを使用するか、...約束する必要があります。

約束

callback hell をVanilla JSに寄せ付けない方法がありますが、Promiseは人気が高まっており、現在ES6で標準化されています( Promise-MDN を参照)。

Promises(a.k.a. Futures)は、非同期コードをより線形で読みやすく提供しますが、それらの機能全体を説明することはこの質問の範囲外です。代わりに、興味のある人のためにこれらの優れたリソースを残します。


JavaScriptの非同期性に関する詳細な資料

  • The Art of Node-Callbacks は、Vanilla JSの例とNode.jsコードでも非同期コードとコールバックを非常によく説明しています。

注:この回答をコミュニティWikiとしてマークしました。したがって、少なくとも100の評判を持っている人なら誰でも編集および改善できます。自由にこの回答を改善するか、必要に応じてまったく新しい回答を送信してください。

この質問をAjaxとは無関係の非同期性の問題に答えるために標準的なトピックに変えたいと思います( AJAX呼び出しから応答を返す方法? があります)、したがってこのトピックでは、可能な限り良い助けになるためにあなたの助けが必要です!

538

Fabrícioの答えはスポットオンです。しかし、非同期性の概念を説明するための類推に焦点を当てた、技術的ではない何かで彼の答えを補完したかったのです


アナロジー...

昨日、私が行っていた仕事には同僚からの情報が必要でした。彼に電話した。会話は次のとおりです。

Me:こんにちはボブ、私たちがどうやってfoo 'd the bar' dを知る必要がある先週。ジムはそれについての報告を望んでおり、あなたはそれについての詳細を知っている唯一の人です。

ボブ:確かに、でも30分くらいかかりますか?

Me:それは素晴らしいボブです。情報が得られたら、リングを返してください!

この時点で、電話を切った。レポートを完成させるためにボブからの情報が必要だったので、レポートを残して代わりにコーヒーを飲みに行き、メールを受け取りました。 40分後(ボブは遅い)、ボブは電話をかけ直し、必要な情報を教えてくれました。この時点で、必要な情報がすべて揃っていたので、レポートで作業を再開しました。


代わりに会話がこのようになったと想像してください。

Me:こんにちはボブ、私たちがどうやってfoo 'd the bar' dを知る必要がある先週。ジムはそれについてのレポートを望んでおり、あなたはそれについての詳細を知っている唯一の人です。

ボブ:確かに、でも30分くらいかかりますか?

Me:それは素晴らしいボブです。待ちます。

そして、私はそこに座って待っていました。そして待った。そして待った。 40分間。待っている以外は何もしません。最終的に、ボブは私に情報を提供し、電話を切り、レポートを完成させました。しかし、40分の生産性が失われました。


これは非同期動作と同期動作です

これが、質問のすべての例で起こっていることです。イメージのロード、ディスクからのファイルのロード、およびAJAXを介したページのリクエストは、すべて遅い操作です(最新のコンピューティングのコンテキストでは)。

JavaScriptでは、これらの遅い操作が完了するのをwaitingではなく、遅い操作が完了したときに実行されるコールバック関数を登録できます。ただし、その間、JavaScriptは引き続き他のコードを実行します。 JavaScriptが他のコードを実行するという事実は、遅い操作が完了するのを待っている間に動作asynchronousになります。 JavaScriptが他のコードを実行する前に操作の完了を待機していた場合、これはsynchronous動作でした。

var outerScopeVar;    
var img = document.createElement('img');

// Here we register the callback function.
img.onload = function() {
    // Code within this function will be executed once the image has loaded.
    outerScopeVar = this.width;
};

// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);

上記のコードでは、JavaScriptにlolcat.pngを読み込むように要求しています。これはsloooow操作です。この遅い操作が完了するとコールバック関数が実行されますが、その間、JavaScriptは次のコード行の処理を続けます。すなわちalert(outerScopeVar)

これが、undefinedを示すアラートが表示される理由です。 alert()は画像が読み込まれた後ではなく、すぐに処理されるためです。

コードを修正するには、alert(outerScopeVar)コードを移動するだけですintoコールバック関数。この結果、グローバル変数として宣言されたouterScopeVar変数は必要なくなりました。

var img = document.createElement('img');

img.onload = function() {
    var localScopeVar = this.width;
    alert(localScopeVar);
};

img.src = 'lolcat.png';

あなたはalwaysコールバックが関数として指定されているのを見るでしょう。 、しかし、後まで実行しません。

したがって、すべての例で、function() { /* Do something */ }はコールバックです。 allの例を修正するには、操作の応答を必要とするコードをそこに移動するだけです!

*技術的にはeval()も使用できますが、この目的で eval()は悪です


発信者を待機させるにはどうすればよいですか?

現在、これに似たコードがあるかもしれません。

function getWidthOfImage(src) {
    var outerScopeVar;

    var img = document.createElement('img');
    img.onload = function() {
        outerScopeVar = this.width;
    };
    img.src = src;
    return outerScopeVar;
}

var width = getWidthOfImage('lolcat.png');
alert(width);

ただし、return outerScopeVarはすぐに発生することがわかっています。 onloadコールバック関数が変数を更新する前。これにより、getWidthOfImage()undefinedを返し、undefinedがアラートになります。

これを修正するには、getWidthOfImage()を呼び出す関数がコールバックを登録できるようにし、幅のアラートをそのコールバック内に移動する必要があります。

function getWidthOfImage(src, cb) {     
    var img = document.createElement('img');
    img.onload = function() {
        cb(this.width);
    };
    img.src = src;
}

getWidthOfImage('lolcat.png', function (width) {
    alert(width);
});

...前と同様に、グローバル変数(この場合はwidth)を削除できたことに注意してください。

146
Matt

これは、クイックリファレンスや、promiseやasync/awaitを使った例を探している人たちのためのもっと簡潔な答えです。

非同期メソッド(この場合はsetTimeout)を呼び出してメッセージを返す関数のための単純なアプローチ(うまくいかない)から始めます。

function getMessage() {
  var outerScopeVar;
  setTimeout(function() {
    outerScopeVar = 'Hello asynchronous world!';
  }, 0);
  return outerScopeVar;
}
console.log(getMessage());

undefinedは、getMessageコールバックが呼び出されてsetTimeoutが更新される前に返されるため、この場合はouterScopeVarがログに記録されます。

それを解決するための2つの主な方法はコールバックプロミスを使うことです:

コールバック

ここでの変更点は、getMessagecallbackパラメータを受け取ることです。このパラメータは、利用可能になったときに結果を呼び出しコードに返すために呼び出されます。

function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});

約束

プロミスは、自然に組み合わせて複数の非同期操作を調整することができるため、コールバックよりも柔軟な代替手段を提供します。 A Promises/A + 標準実装は、node.js(0.12+)および現在の多くのブラウザでネイティブに提供されていますが、 Bluebird および _ q _ のようなライブラリでも実装されています。

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

getMessage().then(function(message) {
  console.log(message);  
});

jQuery 延期

jQueryは、そのDeferredとの約束に似た機能を提供します。

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

非同期/待機

JavaScript環境に async および await のサポートが含まれている場合(Node.js 7.6以降のように)、async関数内で同期的にpromiseを使用できます。

function getMessage () {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve('Hello asynchronous world!');
        }, 0);
    });
}

async function main() {
    let message = await getMessage();
    console.log(message);
}

main();
66
JohnnyHK

明らかに言うと、カップはouterScopeVarを表します。

非同期関数は以下のようになります...

asynchronous call for coffee

51

他の答えは素晴らしいです、そして私はただこれに簡単な答えを提供したいです。 jQueryの非同期呼び出しだけに制限する

すべてのajax呼び出し($.get$.post、または$.ajaxを含む)は非同期です。

あなたの例を考える

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

コードの実行は1行目から開始し、変数を宣言して2行目で非同期呼び出し(トリガー要求)をトリガーし、3行目から実行を継続します。ポスト要求が実行を完了するのを待つことはありません。

投稿要求が完了するのに10秒かかるとしましょう。outerScopeVarの値はそれらの10秒後にのみ設定されます。

試してみると、

var outerScopeVar; //line 1
$.post('loldog', function(response) {  //line 2, takes 10 seconds to complete
    outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun");  //line 3
alert(outerScopeVar);  //line 4

これを実行すると、3行目にアラートが表示されます。投稿要求が何らかの値を返したことを確認するまでしばらくお待ちください。アラートボックスで[OK]をクリックすると、待っていたため、次のアラートで予想値が印刷されます。

実際のシナリオでは、コードは次のようになります。

var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
    alert(outerScopeVar);
});

非同期呼び出しに依存するすべてのコードは、非同期ブロック内に移動するか、非同期呼び出しを待つことによって移動します。

13
Teja

これらすべてのシナリオで、outerScopeVarが変更されるか、値が割り当てられます 非同期的に または 後で発生する(何らかのイベントの発生を待機または待機している)現在の実行は待機しません 現在の実行フローはouterScopeVar = undefinedになります

それぞれの例について説明しましょう(非同期的に呼び出される部分、またはイベントが発生するのを遅らせる部分をマークしました)。

1.

enter image description here

ここでは、その特定のイベントで実行されるeventlistnerを登録します。imageの読み込み中。現在の実行が次の行img.src = 'lolcat.png';alert(outerScopeVar);で継続している間は、イベントは発生しません。すなわち、funtion img.onloadは、参照された画像が非同期に読み込まれるのを待ちます。これは以下のすべての例で起こります - イベントは異なるかもしれません。

2.

2

ここではタイムアウトイベントが役割を果たし、指定された時間が経過するとハンドラが呼び出されます。ここでは0ですが、それでも実行のためにEvent Queueの最後の位置に追加される非同期イベントを登録するため、保証された遅延になります。

3.

enter image description here 今回はAjaxのコールバック。

4.

enter image description here

ノードは非同期コーディングの王と見なすことができます。ここでマークされた関数は、指定されたファイルを読み込んだ後に実行されるコールバックハンドラとして登録されています。

5.

enter image description here

明らかな約束(何かが将来行われるでしょう)は非同期です。参照 JavaScriptでの延期、約束、未来の違いは何ですか?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

10
Tom Sebastian