0

I'm writing an article about JS concurrency model and event loop. I've read many (even internally) conflicting articles regarding the data structures of the call stack and Execution Contexts.

A JavaScript engine like Google's V8 is composed of two parts: the call stack and the heap. The stack is made up of Execution Contexts that hold information about the currently running function. When a function returns another function a closure is created around the returned function, i.e. the returned function now has memory associated with it.

For example:

function counterCreator() {
    let counter = 0
    return function count() {
        console.log(counter)
        return counter++
    }
}
const counter1 = counterCreator()
counter1() // 0
counter1() // 1
counter1() // 2

const counter2 = counterCreator()
counter2() // 0
counter2() // 1
counter2() // 2

When the code executes:

  • Global Execution Context data structure is created
    • the contents of the counterCreator function are stored on the heap and a pointer to it is stored in counterCreator
    • counter1 is assigned undefined
    • counter2 is assigned undefined
  • Global Execution Context is executed
    • The counterCreator() function call is found, so the JS engine pushes a new Execution Context to the call stack
      • counterCreator() Execution Context is created
        • counter is assigned undefined
        • the contents of the count() function are stored on the heap and a pointer to it is stored in count
      • counterCreator() Execution Context is run
        • counter is assigned 0
        • What happens on this return statement?

If the counterCreator() function returned a primitive data type (integer, float, undefined, null, etc), it would return that data type and be done. If the counterCreator() function returned a non-primitive data type (object or array), those non-primitive data types would be stored in the heap and it would return a pointer to their place in memory.

But what happens if the counterCreator() function returns another function? A closure with the variable counter needs to wrap around the returned function.

How is this closure stored in memory? What's the mechanism for the closured function to access it's closure variables? Is the call stack made up of Execution Contexts or merely pointers to Execution Contexts stored in the heap?

I've read several explanations:

  • The closure never leaves the stack - doesn't make sense to me
  • The returned function's parent Execution Context (minus the variables the returned function doesn't reference, for efficiency) is copied to the heap and copied back into the call stack when the returned function is invoked, i.e. when counter1() is ran - makes sense, but doesn't seem efficient
  • All returned functions have an internal variable called [[scope]] (that is inaccessible through code) that references the closure scope - but this still doesn't explain the closure's data structure, which I assume lives in the heap
  • During the creation of the counterCreator() Execution Context, not just the contents of count() are stored in the heap, but also any variables that the function references that might live in the parent scope (as if the function received them as arguments).

Would appreciate thorough clarifications on this. The closest to the memory layout, the better.

André Casal
  • 1,012
  • 1
  • 11
  • 25
  • this sounds like a question for the Software Engineering exchange? – maraaaaaaaa Jan 15 '22 at 00:24
  • Not sure if this helps, but yes, the returned function has an internal slot called `[[Scope]]` (newer versions of the spec seem to call it [`[[Environment]]`](https://262.ecma-international.org/12.0/#table-internal-slots-of-ecmascript-function-objects)), that holds the execution contexts "Lexical Environment" component that the function was created in. So the `[[Environment]]` slot is set to the Environment Record (that is held in the Lexical Environment component of the execution context), whose structure is described [here](https://262.ecma-international.org/12.0/#sec-environment-records). – Nick Parsons Jan 15 '22 at 00:34
  • 1
    "*The closest to the memory layout, the better*" - if that's your goal, pick a particular engine (most of them are open source) and dig into the specific implementation. They're all different. – Bergi Jan 15 '22 at 01:06
  • 1
    "*A JavaScript engine like Google's V8 is composed of two parts: the call stack and the heap*" - if you're talking about concrete memory regions, no; there are more details. In a very abstract - almost meaningless - way, maybe. But you might as well just treat "the stack" as yet another object on the heap. The ECMAScript standard doesn't care, JS does not let the user program specify the memory layout. – Bergi Jan 15 '22 at 01:09
  • 1
    As@NickParsons hinted, if you want to use ECMAScript terminology like "execution context" (and the spec abstractions), you will need to distinguish that from the environment records that actually hold the variable values. – Bergi Jan 15 '22 at 01:12
  • 1
    "*what happens if the `counterCreator()` function returns another function?*" - well, it simply returns the "pointer to the function" that is stored in the variable `count`. The actual function object is stored on the heap as you say; let the garbage collector figure out the rest. – Bergi Jan 15 '22 at 01:14
  • 1
    possible duplicate of [Where does a JavaScript closure live?](https://stackoverflow.com/q/37491626/1048572) and the other questions [linked there](https://stackoverflow.com/questions/37491626/where-does-a-javascript-closure-live?noredirect=1&lq=1#comment62492865_37491626) – Bergi Jan 15 '22 at 01:18

0 Answers0