Perché la mia variabile rimane inalterata dopo averla modificata all'interno di una funzione? - Riferimento al codice asincrono

May 15 2014

Dati i seguenti esempi, perché è outerScopeVarindefinito in tutti i casi?

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

Perché viene visualizzato undefinedin tutti questi esempi? Non voglio soluzioni alternative, voglio sapere perché sta accadendo.


Nota: questa è una domanda canonica per l' asincronicità di JavaScript . Sentiti libero di migliorare questa domanda e aggiungere esempi più semplificati con cui la comunità può identificarsi.

Risposte

605 FabrícioMatté May 15 2014 at 06:55

Risposta in una parola: asincronicità .

Prefazioni

Questo argomento è stato ripetuto almeno un paio di migliaia di volte, qui, in Stack Overflow. Quindi, prima di tutto vorrei segnalare alcune risorse estremamente utili:


La risposta alla domanda in questione

Tracciamo prima il comportamento comune. In tutti gli esempi, outerScopeVarviene modificato all'interno di una funzione . Quella funzione chiaramente non viene eseguita immediatamente, viene assegnata o passata come argomento. Questo è ciò che chiamiamo callback .

La domanda è: quando viene chiamata la richiamata?

Dipende dal caso. Proviamo di nuovo a tracciare un comportamento comune:

  • img.onloadpotrebbe essere richiamato in futuro , quando (e se) l'immagine è stata caricata correttamente.
  • setTimeoutpuò essere chiamato in futuro , dopo che il ritardo è scaduto e il timeout non è stato annullato da clearTimeout. Nota: anche quando si utilizza 0come ritardo, tutti i browser hanno un limite di ritardo di timeout minimo (specificato per essere 4 ms nelle specifiche HTML5).
  • $.postLa richiamata di jQuery potrebbe essere chiamata in futuro , quando (e se) la richiesta Ajax sarà stata completata con successo.
  • Node.js fs.readFilepotrebbe essere chiamato in futuro , quando il file è stato letto con successo o ha generato un errore.

In tutti i casi, abbiamo una richiamata che potrebbe essere eseguita in futuro . Questo "in futuro" è ciò che chiamiamo flusso asincrono .

L'esecuzione asincrona viene espulsa dal flusso sincrono. Ovvero, il codice asincrono non verrà mai eseguito durante l'esecuzione dello stack di codice sincrono. Questo è il significato di JavaScript a thread singolo.

Più specificamente, quando il motore JS è inattivo, non eseguendo uno stack di (a) codice sincrono, eseguirà il polling per eventi che potrebbero aver attivato callback asincroni (ad esempio timeout scaduto, risposta di rete ricevuta) e li eseguirà uno dopo l'altro. Questo è considerato come Event Loop .

Cioè, il codice asincrono evidenziato nelle forme rosse disegnate a mano può essere eseguito solo dopo che tutto il codice sincrono rimanente nei rispettivi blocchi di codice è stato eseguito:

In breve, le funzioni di callback vengono create in modo sincrono ma eseguite in modo asincrono. Non puoi fare affidamento sull'esecuzione di una funzione asincrona finché non sai che è stata eseguita, e come farlo?

È semplice, davvero. La logica che dipende dall'esecuzione della funzione asincrona dovrebbe essere avviata / chiamata dall'interno di questa funzione asincrona. Ad esempio, spostando anche alerts ed console.logs all'interno della funzione di callback si produrrebbe il risultato atteso, perché il risultato è disponibile a quel punto.

Implementazione della propria logica di callback

Spesso è necessario fare più cose con il risultato di una funzione asincrona o fare cose diverse con il risultato a seconda di dove è stata chiamata la funzione asincrona. Affrontiamo un esempio un po 'più complesso:

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

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

Nota: sto usando setTimeoutcon un ritardo casuale come funzione asincrona generica, lo stesso esempio si applica ad Ajax readFile, onloade qualsiasi altro flusso asincrono.

Questo esempio soffre chiaramente dello stesso problema degli altri esempi, non è in attesa che venga eseguita la funzione asincrona.

Affrontiamolo implementando un nostro sistema di callback. Prima di tutto, ci liberiamo di quel brutto outerScopeVarche è completamente inutile in questo caso. Quindi aggiungiamo un parametro che accetta un argomento di funzione, il nostro callback. Al termine dell'operazione asincrona, chiamiamo questo callback passando il risultato. L'implementazione (si prega di leggere i commenti in ordine):

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

Snippet di codice dell'esempio precedente:

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

Molto spesso nei casi d'uso reali, l'API DOM e la maggior parte delle librerie forniscono già la funzionalità di callback (l' helloCatAsyncimplementazione in questo esempio dimostrativo). Devi solo passare la funzione di callback e capire che verrà eseguita al di fuori del flusso sincrono e ristrutturare il tuo codice per adattarlo.

Noterai anche che a causa della natura asincrona, è impossibile returnriportare un valore da un flusso asincrono al flusso sincrono in cui è stato definito il callback, poiché i callback asincroni vengono eseguiti molto tempo dopo che il codice sincrono ha già terminato l'esecuzione.

Invece di returninserire un valore da una richiamata asincrona, sarà necessario utilizzare il modello di richiamata o ... Promesse.

Promesse

Sebbene ci siano modi per tenere a bada l' inferno di richiamata con vanilla JS, le promesse stanno crescendo in popolarità e sono attualmente standardizzate in ES6 (vedere Promise - MDN ).

Le promesse (note anche come Futures) forniscono una lettura più lineare, e quindi piacevole, del codice asincrono, ma spiegare la loro intera funzionalità è fuori dallo scopo di questa domanda. Invece, lascio queste eccellenti risorse per gli interessati:


Altro materiale da leggere sull'asincronicità di JavaScript


Nota: ho contrassegnato questa risposta come Community Wiki, quindi chiunque abbia almeno 100 reputazioni può modificarla e migliorarla! Sentiti libero di migliorare questa risposta o di inviare una risposta completamente nuova se lo desideri.

Voglio trasformare questa domanda in un argomento canonico per rispondere a problemi di asincronicità che non sono correlati ad Ajax (c'è Come restituire la risposta da una chiamata AJAX? Per quello), quindi questo argomento ha bisogno del tuo aiuto per essere il più buono e utile possibile !

158 Matt May 29 2014 at 16:09

La risposta di Fabrício è perfetta; ma volevo completare la sua risposta con qualcosa di meno tecnico, che si concentra su un'analogia per aiutare a spiegare il concetto di asincronicità .


Un'analogia ...

Ieri il lavoro che stavo facendo richiedeva alcune informazioni da un collega. L'ho chiamato; ecco come è andata la conversazione:

Io : Ciao Bob, ho bisogno di sapere come abbiamo trovato il bar la scorsa settimana. Jim vuole un rapporto al riguardo e tu sei l'unico a conoscerne i dettagli.

Bob : certo, ma mi ci vorranno circa 30 minuti?

Io : è fantastico Bob. Dammi un richiamo quando hai le informazioni!

A questo punto, ho riattaccato il telefono. Dal momento che avevo bisogno di informazioni da Bob per completare il mio rapporto, ho lasciato il rapporto e sono andato a prendere un caffè, poi ho recuperato qualche e-mail. 40 minuti dopo (Bob è lento), Bob ha richiamato e mi ha fornito le informazioni di cui avevo bisogno. A questo punto, ho ripreso il mio lavoro con la mia relazione, poiché avevo tutte le informazioni di cui avevo bisogno.


Immagina se invece la conversazione fosse andata così;

Io : Ciao Bob, ho bisogno di sapere come abbiamo trovato il bar la scorsa settimana. Jim vuole un rapporto al riguardo, e tu sei l'unico a conoscerne i dettagli.

Bob : certo, ma mi ci vorranno circa 30 minuti?

Io : è fantastico Bob. Aspetterò.

E mi sono seduto lì e ho aspettato. E ho aspettato. E ho aspettato. Per 40 minuti. Non fare altro che aspettare. Alla fine, Bob mi ha dato le informazioni, abbiamo riattaccato e ho completato il mio rapporto. Ma avevo perso 40 minuti di produttività.


Questo è un comportamento asincrono rispetto a quello sincrono

Questo è esattamente ciò che sta accadendo in tutti gli esempi nella nostra domanda. Il caricamento di un'immagine, il caricamento di un file dal disco e la richiesta di una pagina tramite AJAX sono tutte operazioni lente (nel contesto dell'informatica moderna).

Anziché attendere il completamento di queste operazioni lente, JavaScript consente di registrare una funzione di callback che verrà eseguita al termine dell'operazione lenta. Nel frattempo, tuttavia, JavaScript continuerà a eseguire altro codice. Il fatto che JavaScript esegua altro codice mentre attende il completamento dell'operazione lenta rende il comportamento asincrono . Se JavaScript avesse atteso il completamento dell'operazione prima di eseguire qualsiasi altro codice, si sarebbe trattato di un comportamento sincrono .

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

Nel codice sopra, chiediamo di caricare JavaScript lolcat.png, che è un'operazione sloooow . La funzione di callback verrà eseguita una volta completata questa lenta operazione, ma nel frattempo JavaScript continuerà a elaborare le successive righe di codice; cioè alert(outerScopeVar).

Questo è il motivo per cui vediamo la visualizzazione dell'avviso undefined; poiché alert()viene elaborato immediatamente, piuttosto che dopo che l'immagine è stata caricata.

Per correggere il nostro codice, tutto ciò che dobbiamo fare è spostare il alert(outerScopeVar)codice nella funzione di callback. Di conseguenza, non abbiamo più bisogno della outerScopeVarvariabile dichiarata come variabile globale.

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

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

img.src = 'lolcat.png';

Vedrai sempre che un callback è specificato come una funzione, perché è l'unico * modo in JavaScript per definire del codice, ma non eseguirlo fino a dopo.

Pertanto, in tutti i nostri esempi, function() { /* Do something */ }è il callback; per correggere tutti gli esempi, tutto ciò che dobbiamo fare è spostare lì il codice che richiede la risposta dell'operazione!

* Tecnicamente puoi usare eval()anche tu , ma eval()è malvagio per questo scopo


Come faccio a mantenere il mio chiamante in attesa?

Al momento potresti avere un codice simile a questo;

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

Tuttavia, ora sappiamo che ciò return outerScopeVaraccade immediatamente; prima che la onloadfunzione di callback abbia aggiornato la variabile. Questo porta a getWidthOfImage()tornare undefinede ad undefinedessere allertato.

Per risolvere questo problema, dobbiamo consentire alla funzione che chiama getWidthOfImage()di registrare un callback, quindi spostare l'avviso della larghezza in modo che sia all'interno di quel callback;

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

... come prima, nota che siamo stati in grado di rimuovere le variabili globali (in questo caso width).

75 JohnnyHK Jan 21 2015 at 06:42

Ecco una risposta più concisa per le persone che cercano un riferimento rapido e alcuni esempi che utilizzano promesse e async / await.

Inizia con l'approccio ingenuo (che non funziona) per una funzione che chiama un metodo asincrono (in questo caso setTimeout) e restituisce un messaggio:

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

undefinedviene registrato in questo caso perché getMessageritorna prima che setTimeoutvenga chiamata la richiamata e si aggiorna outerScopeVar.

I due modi principali per risolverlo sono l'utilizzo di callback e promesse :

Richiami

La modifica qui è che getMessageaccetta un callbackparametro che verrà chiamato per restituire i risultati al codice chiamante una volta disponibile.

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

Promesse

Le promesse forniscono un'alternativa più flessibile rispetto ai callback perché possono essere combinate naturalmente per coordinare più operazioni asincrone. A Promesse / A + implementazione standard viene fornito in modo nativo node.js (0.12+) e molti browser attuali, ma è implementata anche nelle biblioteche come la Bluebird e 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 fornisce funzionalità simili alle promesse con i suoi Deferred.

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

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

asincrono / attendono

Se il tuo ambiente JavaScript include il supporto per asynce await(come Node.js 7.6+), puoi utilizzare le promesse in modo sincrono all'interno delle asyncfunzioni:

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();
58 JohannesFahrenkrug Dec 08 2015 at 23:48

Per affermare l'ovvio, la tazza rappresenta outerScopeVar.

Le funzioni asincrone sono come ...

14 Teja Feb 26 2016 at 10:59

Le altre risposte sono eccellenti e voglio solo fornire una risposta diretta a questo. Limitando solo alle chiamate asincrone jQuery

Tutte le chiamate ajax (incluso $.getor $.postor $.ajax) sono asincrone.

Considerando il tuo esempio

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

L'esecuzione del codice inizia dalla riga 1, dichiara la variabile e innesca e la chiamata asincrona sulla riga 2, (cioè la richiesta di post) e continua la sua esecuzione dalla riga 3, senza attendere che la richiesta di post completi la sua esecuzione.

Diciamo che la richiesta di post richiede 10 secondi per essere completata, il valore di outerScopeVarverrà impostato solo dopo quei 10 secondi.

Da provare

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

Ora, quando lo esegui, riceverai un avviso sulla riga 3. Attendi un po 'di tempo finché non sei sicuro che la richiesta di post ha restituito un valore. Quindi, quando si fa clic su OK, nella casella di avviso, l'avviso successivo stamperà il valore previsto, perché lo si aspettava.

Nello scenario di vita reale, il codice diventa,

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

Tutto il codice che dipende dalle chiamate asincrone, viene spostato all'interno del blocco asincrono, o in attesa delle chiamate asincrone.

11 TomSebastian Oct 27 2015 at 13:35

In tutti questi scenari outerScopeVarviene modificato o assegnato un valore in modo asincrono o che accade in un secondo momento (in attesa o in ascolto del verificarsi di qualche evento), per il quale l'esecuzione corrente non attenderà . Quindi tutti questi casi il flusso di esecuzione corrente risulta inouterScopeVar = undefined

Discutiamo ogni esempio (ho contrassegnato la parte che viene chiamata in modo asincrono o ritardata per il verificarsi di alcuni eventi):

1.

Qui registriamo un eventlistner che verrà eseguito su quel particolare evento. Qui viene caricata l'immagine. Quindi l'esecuzione corrente continua con le righe successive img.src = 'lolcat.png';e alert(outerScopeVar);nel frattempo l'evento potrebbe non verificarsi. cioè, la funzione img.onloadattende il caricamento dell'immagine di riferimento, in modo asincrono. Questo accadrà nel seguente esempio: l'evento potrebbe essere diverso.

2.

Qui l'evento timeout gioca il ruolo, che richiamerà il gestore dopo il tempo specificato. Eccolo 0, ma ancora registra un evento asincrono che verrà aggiunto all'ultima posizione di Event Queuefor execution, il che rende il ritardo garantito.

3.

Questa volta ajax callback.

4.

Il nodo può essere considerato come un re della codifica asincrona.Qui la funzione contrassegnata è registrata come un gestore di callback che verrà eseguito dopo aver letto il file specificato.

5.

La promessa ovvia (qualcosa sarà fatto in futuro) è asincrona. vedi Quali sono le differenze tra Deferred, Promise e Future in JavaScript?

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