2 years ago
#53349

André Casal
JavaScript: What is the data structure for the call stack?
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?
- counterCreator() Execution Context is created
- The counterCreator() function call is found, so the JS engine pushes a new Execution Context to the call stack
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.
javascript
data-structures
callstack
executioncontext
javascript-engine
0 Answers
Your Answer