skip to Main Content

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


  1. it seems that yes, it is the same

        let array = ['1','2','3'];
    
        array.forEach( e => {
    
            const foo = {
                id: e
            };
    
            setTimeout(() => {
              const secureFoo = foo.id;
    
              console.log(`${secureFoo} is the same`);
            }, 2000);
    
        })
    
    Login or Signup to reply.
  2. is there a guarantee that callback function will use the const foo declared immediately before it

    TL; DR; Yes.

    The reference to socket.io is irrelevant. The guarantee comes from JS itself.


    Each time

    const foo = { ... }
    

    and

    (err, clients) => { foo.clients = clients })
    

    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:

    for(var i = 0 ; i != 3 ; ++i) setTimeout(() => console.log("i =", i), 0)
    

    this will output "i = 3" tree times.

    But any of the following will give you the expected result:

    1. let/const:

      for(let i = 0 ; i != 3 ; ++i) setTimeout(() => console.log("i =", i), 0)
      

      That’s because let (and const) behaves differently from var. See MDN:

      let allows you to declare variables that are limited to the scope of a block statement, or expression on which it is used, unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope

    2. forEach:

      [1,2,3].forEach((v, i) => setTimeout(() => console.log("i =", i), 0) )
      

      That’s because arguments of each call of forEach callback are actually different variables

    3. And even var inside a function scope:

      [1,2,3].forEach((v, i_) => { var i = i_; setTimeout(() => console.log("i =", i), 0) } )
      

      That’s because var i = i_ declares a new variable local to each call of forEach callback.

      But it’s not so in this case:

      for(let i_ = 0 ; i_ != 3 ; ++i_) {
        var i = i_
        setTimeout(() => console.log("i =", i), 0)
      }
      

      Because var is hoisted. So the previous code is equivalent to

      var i
      for(let i_ = 0 ; i_ != 3 ; ++i_) {
        i = i_
        setTimeout(() => console.log("i =", i), 0)
      }
      
    Login or Signup to reply.
  3. Well, that’s how closure works.

      io.in(element.id).clients((err, clients) => {
        foo.clients = clients;
      });
    

    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.

    [[scopes]]
    0:Closure (outer)
        foo: ....//reference to foo
    

    So inside the code, a reference to foo is always going to be there.

    Login or Signup to reply.
  4. 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 a const (which is block scoped) within a function (which qualifies as a block), so unless foo 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:

    1. Code runs within an execution context—essentially a representation of the local environment in which it is evaluated.
    2. Global code is run in the global execution context. Every time a new function is called, a new execution context is created.
    3. Every execution context has an associated lexical environment, which is where any identifiers (i.e. parameters, variables and function names) declared within that function are associated with values.
    4. Every lexical environment maintains a link to its outer lexical environment—for a function, this is the lexical environment in which the function itself was declared, not called.
    5. When you reference an identifier in an execution context, the implementation will first look for it in the context’s own lexical environment. If it isn’t found there, it will look in the outer lexical environment, and so on until the global lexical environment is reached.

    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:

    /*
    Outer lexical environment (possibly global).
    Identifiers elements and io declared here or in an outer lexical environment.
    */
    
    elements.forEach((element) => {
      /*
      New lexical environment associated with forEach callback.
      Links to outer lexical environment.
      */
    
      // Identifier foo declared in the forEach callback's lexical environment.
      const foo = {
        // References identifier element from an outer lexical environment.
        id: element.id,
        name: element.name,
      };
    
      io.in(element.id).clients((err, clients) => {
        /*
        Lexical environment associated with clients callback.
        Links to lexical environment for the forEach callback.
        */
    
        // Reference to identifier in the forEach callback's lexical environment.
        foo.clients = clients;
      });
    });
    

    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 the forEach callback in which it was declared—where foo 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 key clients existing in foo within that callback—not outside of it.

    elements.forEach(({ id, name }) => {
      const foo = { id, name };
    
      io.in(id).clients((err, clients) => {
        foo.clients = clients;
    
        // 'clients' in foo === true
      });
    
        // 'clients' in foo === false
    });
    
    // foo is not declared here
    

    This may or may not be intentional, but it’s definitely outside the scope of this question!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search