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


669

次の例を考えると、なぜ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の非同期性に関する標準的な質問です。この質問を自由に改善し、コミュニティが特定できるより簡単な例を追加してください。



@Dukelingに感謝します。そのリンクでコメントしたことは確かですが、コメントが足りないようです。また、編集に関して:タイトルに「標準」と「非同期」が含まれていると、この質問を検索して別の質問をだましとしてマークするのに役立ちます。そしてもちろん、非同期性の説明を探すときにGoogleからこの質問を見つけるのにも役立ちます。
ファブリシオマッテ

3
もう少し考えてみると、「標準的な非同期性のトピック」はタイトルで少し重く、「非同期コード参照」はよりシンプルで客観的です。また、ほとんどの人が「非同期性」ではなく「非同期」を検索すると考えています。
ファブリシオマッテ

1
関数を呼び出す前に変数を初期化する人もいます。どういうわけかそれを表すタイトルを変更してみてはいかがですか?「変数を関数内で変更した後に変更されないのはなぜですか?」?
フェリックスクリング14

上記のすべてのコード例で、「alert(outerScopeVar);」NOWを実行しますが、「outerScopeVar」への値の割り当てはLATER(非同期)で行われます。
リファクタリング

回答:


542

一言で言うと、非同期性です。

序文

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


手元の質問に対する答え

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

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

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

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

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

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

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

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

ハイライトされた非同期コード

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

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

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

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

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

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

注意:私が使用しているsetTimeout一般的な非同期関数としてランダム遅延と、同じ例では、アヤックスに適用されreadFileonloadおよび任意の他の非同期の流れ。

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

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

// 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、非同期コールバックは同期コードの実行が完了してからかなり後に実行されるため、非同期フローからコールバックが定義された同期フローに戻ることは不可能です。

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

約束

バニラJSでコールバックを寄せ付けない方法がありますが、約束は人気が高まっており、現在ES6で標準化されています(約束 -MDNを参照)。

Promise(別名Futures)は、より線形で、したがって快適な非同期コードの読み取りを提供しますが、機能全体を説明することはこの質問の範囲外です。代わりに、興味のある人のためにこれらの優れたリソースを残します。


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

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

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

私はこの質問を標準的なトピックに変えて、Ajaxとは無関係の非同期性の問題に答えたいと思います(そのためにAJAX呼び出しから応答を返す方法はありますか?) !


1
あなたの最後の例では、匿名関数を使用する特定の理由がありますか、名前付き関数を使用しても同じように機能しますか?
JDelage

1
関数を呼び出した後に宣言しているため、コード例は少し奇妙です。もちろん巻き上げのために動作しますが、それは意図的でしたか?
ベルギ

2
デッドロックですか?フェリックス・クリングはあなたの答えを指しているし、あなたはフェリックスの答えを指している
マヒ

1
赤い丸のコードはネイティブの非同期javascript関数によって実行されているため、非同期であることを理解する必要があります。これは、java.scriptエンジンの機能です(Node.jsかブラウザーかにかかわらず)。本質的にブラックボックス(Cなどで実装)である関数に「コールバック」として渡されるため、非同期です。不幸な開発者にとって、彼らは非同期です...という理由だけで。独自の非同期関数を作成する場合は、SetTimeout(myfunc、0)に送信してハッキングする必要があります。あなたはそれをすべきですか?別の議論....おそらくない。
ショーンアンダーソン

@Fabricio私は「> = 4msのクランプ」を定義する仕様のために見てきましたが、それを見つけることができなかった-私は、MDNの(ネストされた呼び出しをクランプするための)類似のメカニズムのいくつかの言及を見つけた- developer.mozilla.org/en-US/docs / Web / API /… -HTML仕様の適切な部分へのリンクがありますか。
セビ

147

ファブリシオの答えはまさにスポットライトです。しかし、私は彼の答えをより技術的でないもので補完したかったのです


アナロジー...

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

:こんにちはボブは、私たちはどのように知っている必要がありますfooの「D バーを先週D」。ジムはそれについての報告を望んでおり、あなたはそれについての詳細を知っている唯一の人です。

ボブ:確かに、でも30分くらいかかる?

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

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


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

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

ボブ:確かに、でも30分くらいかかる?

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

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


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

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

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

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);

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

これが、アラートが表示される理由undefinedです。以来、alert()すぐに処理されるのではなく、イメージがロードされた後。

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

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

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

img.src = 'lolcat.png';

コールバックは関数として指定されていることが常にわかります。これは、JavaScriptでコードを定義する唯一の方法であるが、後まで実行しないためです。

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

*技術的には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()戻りundefinedundefinedアラートが発生します。

これを修正するには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)を削除できたことに注意してください。


しかし、結果を別の計算で使用したり、オブジェクト変数に保存したりする場合、アラートやコンソールへの送信はどのように役立ちますか?
ケンイングラム

68

クイックリファレンスと、promisesおよびasync / awaitを使用したいくつかの例を探している人のための、より簡潔な答えを以下に示します。

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

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

undefinedので、この場合には記録されますgetMessage前に戻りsetTimeout、コールバックが呼び出さと更新されますouterScopeVar

これを解決する主な2つの方法は、コールバックプロミスを使用することです。

コールバック

ここでの変更は、使用可能な場合に呼び出し元のコードに結果を返すために呼び出されるパラメーターをgetMessage受け入れるcallbackことです。

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

約束

Promiseは、自然に組み合わせて複数の非同期操作を調整できるため、コールバックよりも柔軟な代替手段を提供します。約束/ A +標準実装では、ネイティブのNode.js(0.12+)と現在の多くのブラウザで提供されるが、同様ライブラリで実装されているブルーバード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は、Deferredsのpromiseと同様の機能を提供します。

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

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

非同期/待機

JavaScript環境にasyncandのサポートが含まれている場合await(Node.js 7.6以降など)、async関数内でプロミスを同期的に使用できます。

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();

Promisesのサンプルは、基本的に過去数時間にわたって私が探していたものです。あなたの例は美しく、同時に約束を説明しています。なぜこれが他のどこにもないのは困惑しています。
ビンセントP

これで問題ありませんが、パラメーターを指定してgetMessage()を呼び出す必要がある場合はどうでしょうか。そのシナリオで上記をどのように記述しますか?
チウダ

2
@Chiwdaコールバックパラメータを最後に置きました:function getMessage(param1, param2, callback) {...}
-JohnnyHK

async/awaitサンプルを試していますが、問題が発生しています。をインスタンス化する代わりにnew Promise、私は.Get()呼び出しを行っているため、どのresolve()メソッドにもアクセスできません。したがって、私getMessage()の結果ではなく、約束を返しています。答えを少し編集して、このための実用的な構文を示してください。
InteXX

@InteXX .Get()電話をかけることの意味がわかりません。おそらく新しい質問を投稿するのが最善です。
JohnnyHK

52

明らかなことを述べるために、カップはを表しouterScopeVarます。

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

コーヒーの非同期呼び出し


13
一方、非同期機能を同期的に動作させようとすると、1秒でコーヒーを飲もうとし、1分で膝に注がれます。
Teepeemm

それが明白なことを述べていた場合、私は質問が尋ねられたとは思わないでしょう、いいえ?
ブロッコリー

2
その:)私は質問が明らかだったということを意味するものではありませんでしたが、カップが図面に何を表すのかは明らかだということで、@ broccoli2000
ヨハネスFahrenkrug

13

他の答えは素晴らしいです、そして、私はちょうどこれに直接的な答えを提供したいです。jQuery非同期呼び出しに制限する

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

あなたの例を考える

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

コードの実行は1行目から始まり、変数を宣言し、2行目でトリガーと非同期呼び出し(つまり、post要求)を実行し、post要求の実行が完了するのを待たずに3行目から実行を続けます。

postリクエストが完了するまでに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);
});

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


or by waiting on the asynchronous callsそれをどうやってやるの?
InteXX

@InteXXコールバックメソッドを使用して
Teja

簡単な構文の例はありますか?
InteXX

10

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

各例について説明しましょう(一部のイベントが発生するために非同期で呼び出されるか、遅延する部分をマークしました)。

1。

ここに画像の説明を入力してください

ここで、その特定のイベントで実行されるeventlistnerを登録します。ここでイメージをロードします。現在の実行は次の行に続きimg.src = 'lolcat.png';alert(outerScopeVar);その間イベントは発生しません。つまり、img.onload参照された画像が非同期に読み込まれるのを待っています。これは、次のすべての例で発生します。イベントは異なる場合があります。

2。

2

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

3。

ここに画像の説明を入力してください 今回はajaxコールバック。

4。

ここに画像の説明を入力してください

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

5。

ここに画像の説明を入力してください

明らかな約束(将来的に何かが行われる)は非同期です。JavaScriptのDeferred、Promise、Futureの違い何ですか?

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.