1. ホーム
  2. javascript

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

2022-03-22 06:07:24

質問

次のような例がありますが、なぜ 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の非同期性 . この質問を改善し、コミュニティが共感できるようなより単純化された例を自由に追加してください。

どのように解決するのですか?

答えは一言。 非同期性 .

序文

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


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

まず、共通の動作をたどってみましょう。すべての例で outerScopeVar の内部で変更されます。 機能 . その関数は明らかにすぐには実行されず、代入されたり引数として渡されたりしています。これが、私たちが コールバック .

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

それはケースによって異なります。もう一度、よくある動作をトレースしてみましょう。

  • img.onload という場合があります。 いつかは 画像の読み込みに成功したとき(そして成功した場合)。
  • setTimeout と呼ばれることがあります。 いつかは によってタイムアウトがキャンセルされなかった場合、遅延時間が経過した後に clearTimeout . 注 0 遅延時間として、すべてのブラウザでタイムアウト遅延の最小値(HTML5 仕様では 4ms と指定されています)が設定されています。
  • jQuery $.post のコールバックが呼び出されることがあります。 将来のある時点で Ajaxリクエストが正常に完了したとき(およびその場合)。
  • Node.jsの fs.readFile と呼ばれることがあります。 将来的には ファイルの読み込みに成功したとき、またはエラーが発生したとき。

すべてのケースで、コールバックが実行されます。 将来のいつか . この「将来のいつか」というのが 非同期フロー .

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

具体的には、JSエンジンがアイドル状態のとき、つまり同期コードのスタックを実行していないとき、非同期コールバックの引き金となったかもしれないイベント(たとえば、タイムアウトが切れた、ネットワーク応答を受け取った)をポーリングして、次々に実行する。これは、次のように見なされます。 イベントループ .

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

要するに、コールバック関数は同期的に作成されるが、非同期的に実行されるのである。ただ、非同期関数の実行は、それが実行されたことがわかるまでは当てにならないし、どうすればいいのか?

実にシンプルです。非同期関数の実行に依存するロジックは、この非同期関数の内部から起動/呼び出しする必要があります。例えば alertconsole.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 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 非同期コールバックから値を取得するには、コールバックパターンを利用する必要があります。プロミス

プロミス

を維持する方法がありますが コールバック地獄 を使用した場合、プロミスはますます人気が高まり、現在ES6で標準化されつつあります( プロミス - MDN ).

プロミス(別名:フューチャー)は、非同期コードをより直線的に、つまり気持ちよく読むことができますが、その機能全体を説明することは、この質問の範囲外です。代わりに、私はこれらの素晴らしいリソースを残しておきます。


JavaScriptの非同期性についてのその他の読み物

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

<ブロッククオート

この回答はコミュニティWikiとして、少なくとも100の評判を持つ誰もが編集し、改善することができます。この回答を改善したり、まったく新しい回答を投稿したりすることもできますので、ご自由にどうぞ。

この質問を正規のトピックにして、Ajaxとは無関係の非同期性の問題に答えたいと思います( AJAX呼び出しからレスポンスを返すには? そのため、このトピックはできるだけ良い、役に立つものにするためにあなたの助けが必要です!