I’m making an async call (https://github.com/socketio/socket.io-redis#redisadapterclientsroomsarray-fnfunction) with socket.io inside of a loop.
elements.forEach((element) => {
const foo = {
id: element.id,
name: element.name,
};
// async code
io.in(element.id).clients((err, clients) => {
foo.clients = clients;
});
});
Since this loop will run to completion before all the async calls complete, is there a guarantee that callback function will use the const foo
declared immediately before it?
4
Answers
it seems that yes, it is the same
TL; DR; Yes.
The reference to
socket.io
is irrelevant. The guarantee comes from JS itself.Each time
and
executed they’re not simple declaring a variable and a function, but create a new variable and a new closure.
Probably your fears come from the common pitfall of
var
:this will output
"i = 3"
tree times.But any of the following will give you the expected result:
let
/const
:That’s because
let
(andconst
) behaves differently fromvar
. See MDN:forEach
:That’s because arguments of each call of
forEach
callback are actually different variablesAnd even
var
inside a function scope:That’s because
var i = i_
declares a new variable local to each call offorEach
callback.But it’s not so in this case:
Because
var
is hoisted. So the previous code is equivalent toWell, that’s how closure works.
Even though the above code is executing asynchronously, in the closure scope, the JavaScript engine will keep the references to variables from its lexical scope necessary to execute it later.
So inside the code, a reference to
foo
is always going to be there.The short answer
Yes, the callback is guaranteed to use the
foo
in the outer function scope.The long answer
The scope of an identifier in ECMAScript is determined by how and where it is declared in your source code; it doesn’t matter if that code is later executed synchronously or asynchronously.
In this case, the identifier
foo
was declared as aconst
(which is block scoped) within a function (which qualifies as a block), so unlessfoo
is redeclared it is in scope anywhere within the opening and closing braces of that function.It’s a bit old and only covers ES5, but I made some slides for a lunch-and-learn about scope in JS a few years back which illustrate declarations and identifier scope visually. (Use the arrow keys to navigate between slides.)
(Separately and beyond the scope of this answer, because you declared it as a
const
it is only defined and accessible in code written after the declaration and assignment.)The really long answer
Here’s a (simplified) rundown of how identifiers are resolved in ECMAScript:
This link back to the lexical environment in which a function was declared is what people mean by a closure. So long as it’s possible that function might be called at some point in a program’s execution, the lexical environment in which it was declared has to be maintained in memory with all associated identifiers and values.
Applying this to your code:
When the
clients
callback is invoked, a new execution context is created with its own lexical environment. That lexical environment links to the outer lexical environment of theforEach
callback in which it was declared—wherefoo
is defined.Still want to know more?
That’s a lot of information to take in, so it’s worth noting: you don’t need to know all this to be a good JS developer! However, it’s fun (and sometimes useful!) to understand exactly how things are supposed to work. For more information about lexical environments, check out Axel Rauschmeyer’s overview or Dmitri Soshnikov’s extremely detailed rundown.
An aside
If the callback is always handled asynchronously, the value of
foo.clients
cannot be relied upon until that callback is known to be resolved. The code you provided doesn’t explicitly do anything to await that callback, so you can only rely on the keyclients
existing infoo
within that callback—not outside of it.This may or may not be intentional, but it’s definitely outside the scope of this question!