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粉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.
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 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
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).
P粉1788942352023-10-14 10:02:51
One word answer: Asynchronicity.
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:
@Felix Kling's answer to "How to return a response from an asynchronous call?". See his excellent answer explaining synchronous and asynchronous processes, as well as the "Reorganizing your code" section.
@Benjamin Gruenbaum also put a lot of effort into explaining asynchronicity within the same thread.
@Matt Esch's answer to "Getting data from fs.readFile" also does a good job of explaining asynchronicity in a simple way.
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). $.post
may be called. 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.
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);
}