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


738

次の例を考えると、なぜ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からこの質問を見つけるのにも役立ちます。
FABRICIOマット

5
もう少し考えてみると、「標準的な非同期トピック」はタイトルが少し重く、「非同期コードリファレンス」はより単純で客観的です。また、ほとんどの人が「非同期性」ではなく「非同期性」を検索していると思います。
FABRICIOマット

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

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

回答:


581

一言で言えば、非同期性です。

序文

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


目の前の質問への答え

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

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

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

  • img.onload画像が正常にロードされた場合(およびその場合)、将来いつか呼び出される可能性があります。
  • setTimeout遅延が終了し、タイムアウトがによってキャンセルされていない後、いつか呼び出される可能性がありますclearTimeout。注:0遅延として使用する場合でも、すべてのブラウザーには最小タイムアウト遅延キャップがあります(HTML5仕様で4msと指定されています)。
  • 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、この場合はまったく役に立たない醜いものを取り除きます。次に、関数の引数を受け入れるパラメーター(コールバック)を追加します。非同期操作が終了したら、このコールバックを呼び出して結果を渡します。実装(コメントを順番に読んでください):

// 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を参照)。

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


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

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

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

この質問を正規トピックに変えて、Ajaxとは関係のない非同期性の問題に答えたい(AJAX呼び出しからの応答を返す方法はありますか?)。したがって、このトピックには、できる限り役立つ役立つ情報が必要です。 !


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

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

2
それはデッドロックです。felix klingがあなたの答えを指しており、felixの答えを指しています
Mahi

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

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

156

ファブリシオの答えはその場にあります。しかし私は、非同期性の概念を説明するのに役立つアナロジーに焦点を当てた、あまり技術的でないもので彼の答えを補足したいと思いました


アナロジー...

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

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

Bob:もちろん。でも30分くらいかかる?

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

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


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

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

Bob:もちろん。でも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);

上記のコードでは、負荷にJavaScriptを求めているlolcat.pngである、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)を削除できることに注意してください。


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

73

ここでは、クイックリファレンスと、promiseとasync / awaitを使用したいくつかの例を探している人のための、より簡潔な回答を示します。

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

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

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

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

コールバック

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

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

約束

プロミスは、複数の非同期操作を調整するために自然に組み合わせることができるため、コールバックよりも柔軟な代替手段を提供します。約束/ 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 Deferreds

jQueryは、Deferredを使用した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のサンプルは基本的に、過去数時間、私が探していたものです。あなたの例は素晴らしく、同時に約束を説明しています。これが他のどこにもない理由は、途方に暮れています。
Vincent P

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

2
@Chiwdaコールバックパラメータを最後に置くだけですfunction getMessage(param1, param2, callback) {...}
JohnnyHK 2017

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

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

56

明白に述べると、カップはを表しouterScopeVarます。

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

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


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

それが明白であると述べていれば、質問はされなかったと思います、いいえ?
broccoli2000

2
@ broccoli2000それによって、質問が明白であるとは意味しませんでしたが、図面でカップが何を表しているのかは明白です:)
Johannes Fahrenkrug

14

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

すべてのajax呼び出し($.getまたは$.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行目にアラートが表示されます。postリクエストが何らかの値を返したことを確認するまでしばらく待ちます。次に、[OK]をクリックすると、アラートボックスで次のアラートが待機するため、期待値が出力されます。

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

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

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


or by waiting on the asynchronous callsどうやってそれを行うのですか?
InteXX

@InteXXコールバックメソッドを使用する
Teja

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

11

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

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

1。

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

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

2。

2

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

3。

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

4。

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

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

5。

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

明白な約束(何かが将来行われる)は非同期です。JavaScriptの据え置き、約束、未来の違い何ですか?

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.