search

Home  >  Q&A  >  body text

Why my variables remain unchanged after modification within a function - Understanding Async Code

Given the following example, why is outerScopeVar undefined in all cases?

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

Why is undefined output in all these examples? I don't need a solution, I want to know why this is happening.


Note: This is a specification question about JavaScript asynchronicity. Please feel free to improve this question and add more simplified examples that the community can agree on.


P粉212114661P粉212114661397 days ago603

reply all(2)I'll reply

  • P粉930534280

    P粉9305342802023-10-14 16:59:42

    Fabrício's answer is very correct; but I would like to supplement his answer with something a little less technical, focusing on helping to explain the concept of asynchronicity through analogies.


    analogy...

    Yesterday, I was working on a job where I needed to get some information from my colleagues. I called him; the conversation went like this:

    Having said that, I hung up the phone. Since I needed the information from Bob to complete my report, I left the report, went to get a cup of coffee, and then I read some emails. 40 minutes later (Bob is slow) Bob called back and gave me the information I needed. At this point, I continue working on my report since I have all the information I need.


    Imagine if the conversation went like this;

    I sat there and waited. and waited. and waited. 40 minutes. Do nothing but wait. Eventually, Bob gave me the information, we hung up, and I finished my report. But I lost 40 minutes of productivity.


    This is asynchronous vs. synchronous behavior

    This is exactly what happens in all the examples in our question. Loading images, loading files from disk, and requesting pages via AJAX are all slow operations (in the context of modern computing).

    JavaScript allows you to register a callback function that will be executed when slow operations complete, instead of waiting for these slow operations to complete. But in the meantime, JavaScript will continue executing other code. The fact that JavaScript is executing other code while waiting for the slow operation to complete makes the behavior asynchronous. If JavaScript waits for an operation to complete before executing any other code, this will be synchronous behavior.

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

    In the above code, we ask JavaScript to load lolcat.png, which is a sloooow operation. Once this slow operation is complete, the callback function will be executed, but in the meantime, JavaScript will continue processing the next line of code; i.e. alert(outerScopeVar).

    This is why we see the alert showing undefined; because alert() is processed immediately, not after the image is loaded.

    To fix our code, all we have to do is move the alert(outerScopeVar) code into the callback function. Therefore, we no longer need to declare the outerScopeVar variable as a global variable.

    var img = document.createElement('img');
    
    img.onload = function() {
        var localScopeVar = this.width;
        alert(localScopeVar);
    };
    
    img.src = 'lolcat.png';

    You will always see callbacks specified as functions because that's the only way in JavaScript to define some code but then execute it later.

    So, in all our examples, function() { /* Do Something */ } is the callback; to fix all examples, all we have to do is change the required The code that operates the response is moved there!

    * Technically you can also use eval(), but eval() is evil for that purpose


    How to put callers on hold?

    You may currently have some code similar to this;

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

    However, we now know that returning outerScopeVar happens immediately; before the onload callback function updates the variable. This causes getWidthOfImage() to return undefined and raise the alert undefined.

    To fix this, we need to allow the function calling getWidthOfImage() to register a callback and then move the width alert inside that 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);
    });

    ...As before, note that we have been able to remove the global variable (width in this case).

    reply
    0
  • P粉178894235

    P粉1788942352023-10-14 10:02:51

    One word answer: Asynchronicity.

    Preface

    This topic has been repeated at least thousands of times on Stack Overflow. So first I'd like to point out some very helpful resources:


    Answers to Current Questions

    Let's trace common behaviors first. In all examples, outerScopeVar is modified inside the function. The function is obviously not executed immediately; it is assigned or passed as a parameter. This is what we call a callback.

    The question now is, when will this callback be called?

    It depends on the specific situation. Let's try tracing some common behaviors again:

    • img.onload Might be called at some time in the future (if) the image has loaded successfully.
    • setTimeout may be called at some time in the future after the delay has expired and the timeout has not been clearTimeout canceled. Note: Even when using 0 as the delay, all browsers have an upper limit on the minimum timeout delay (specified as 4 milliseconds in the HTML5 specification).
    • When (and if) the Ajax request has completed successfully, At some point in the future the callback for jQuery $.post may be called.
    • Node.js's fs.readFile may be called at some time in the future when the file has been successfully read or an error has been thrown.

    In all cases, we have a callback that may be run at some time in the future. This "sometime in the future" is what we call Asynchronous Stream.

    Asynchronous execution is pushed out of the synchronous process. That is, while the synchronous code stack is executing, the asynchronous code will forever execute. This is what JavaScript is single-threaded about.

    More specifically, when the JS engine is idle - not executing a bunch of (a)synchronous code - it will poll for events that might trigger an asynchronous callback (e.g. timeout, network response received) and execute them one after another . This is considered an event loop.

    That is, the asynchronous code highlighted in the hand-drawn red shape can only execute after all remaining synchronous code in its respective code block has executed:

    In short, callback functions are created synchronously but executed asynchronously. You can't rely on the execution of an async function until you know it has executed, how do you do this?

    It's really simple. Logic that relies on the execution of an async function should be initiated/called from within that async function. For example, moving alert and console.log inside the callback function will output the expected results because the results are available at that time.

    Implement your own callback logic

    Often, you need to perform more operations on the result of an async function, or perform different operations on the result depending on where the async function is called. Let's deal with a more complex example:

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

    Note: I'm using setTimeout with a random delay as a generic async function; the same example works for Ajax, readFile, onload and any other asynchronous flow.

    This example obviously has the same problem as the other examples; it doesn't wait until the async function executes.

    Let's solve this problem by implementing our own callback system. First, we get rid of that ugly outerScopeVar which is completely useless in this case. Then we add a parameter that accepts a function argument, our callback. When the asynchronous operation completes, we call this callback and pass the result. Implementation (please read the comments in order):

    // 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 an 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 an argument
            callback('Nya');
        }, Math.random() * 2000);
    }

    Code snippet for the above example:

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

    reply
    0
  • Cancelreply