JavaScript Memory Management

In this chapter we'll cover how JavaScript variables are stored in memory. How scope works and what happens when a variable falls out of scope. We'll look at higher order functions and how JavaScript facilitates their use. Finally we'll cover closures and how JavaScript breaks some conventions about scope.

 

These notes have so many holes in them, they might be considered swiss cheese. The concepts are difficult to translate to still images, and while I try my best; losing the order in which cells are added makes these notes very difficult to follow. They are meant as a supplement for attending class, not a replacement

 

Call Stack and Heap

JavaScript like most other languages, stores it's data in two places during execution. The (call) stack and the heap. Things that have fixed sizes are allocated on the stack. In JS this mostly refers to primitives

The heap is used to store all dynamically allocated variables: objects (including functions)

 

Strings technically don't follow this convention under the hood, but through implementation magic we can treat them as if they are fixed size.

Case 1

 

scope01

 

We can see primitive types treat assignment as pass by value, a new memory address is created and the value is copied over.

 

Case 2

 

scope02

Pass By Copy of a Reference

JavaScript uses pass by Copy of a Reference (also called: call by sharing)

This is effectively pass by value, with the added caveat that objects are stored on the call stack as pointers to the heap.

When you copy a object, you are passing the memory address as the value.

 

Case 3

scope03

 

Both f1 and f2 are referencing the same object on the heap. Any changes made by f1 also effect f2

This is contrast to Case 2, where a new object is created (right hand side evaluation is when the object is added to the heap) and that heap address is assigned to the value of f1.

 

Case 4

scope04

 

Functions are just a specialized object. It gets stored on the heap like any other object.

 

Case 5

Largely the same, but the inner object toDecimal is evaluated first.

scope05

 

Copying an Object (Shallow Copy)

Copying objects by assignment copies the reference:

If instead if we want each of the values to be independent, we can create a new object and populate it with the same property value pairs using the spread operator.

scope06

{...f1} evaluates to {...0xA1} evaluates to {...{num:3, den:4}} evaluates to {num:3, den:4}

This value gets stored into 0xA2 which is finally assigned to the value of f2

 

Case 5

shallowcopy

Limitations

This is called a shallow copy because it only works as a copy by value for objects that do not have other objects nested inside it. If we have a nested objects the reference to the nested object is copied over.

 

Deep Copy (Clone)

Not really important to us, but here are two solutions to create a deep copy function.

Hacking JSON (Legacy Solution)

The quickest solution prior to 2022 was a combination of JSON.stringify to turn an object into a JSON string followed immediately by JSON.parse to turn it back into an object. You will see this very often.

Aside from obvious inefficiencies, there were several problems with this including the fact that JSON only supports simple key value pair objects and arrays. It doesn't support complex objects like Functions and Binary data (which get dropped), and other unsupported types like Dates are converted to strings, which don't convert back to Dates during the deserialization process.

 

Structured Clone

Recent Addition 2022. Might not be available in all browsers. Should be fine server side.

https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

 

 

Scopes

Scope determines the accessibility of variables. In JavaScript we have 4 different types of scope.

We will be interested in 3 of these: Global, Function, and Block Scope

Note scope in a browser works slightly differently so these rules only apply to Node.js

Global scope

In Node.js variables defined without a let, const, or var are global.

 

 

With global scope, once a variable is declared as global, it can be used anywhere in the program. (Including in separate files)

 

 

Using Global scope is generally considered bad practice and should be avoided for most developers except to development related to implementations of the language itself (eg. when working on a browser feature or contributing code to the Node.js project, you want those new functions available to all developers so you would put them in the global namespace)

The problem with global variables is they cause namespace pollution, preventing the usage of specific variable names in a file. In Node.js If you want to import variables from another file there are more appropriate ways like module.export.

 

Exporting file

Importing file

 

Function Scope

In Node.js variables declared with the var keyword have function scope. These variables are scoped to the function they are defined inside.

 

scope07

When myfunc(5) is called a new stack frame is added. This stack frame is removed when the function returns freeing the memory.

What are Stack Frames:

https://youtu.be/Q2sFmqvpBe0

Top Level Function Scope

If variables are defined on the top level of the program they are scoped to the file

Nesting Scope

In JavaScript both Function and Lexical Scope nests, inner functions can access the outer functions variables, but not the reverse.

Hoisting

Why does the following code prints undefined,

But this one Errors?

Function scoped variables have their declarations (not assignments) moved to the top of the function in which they are defined (or for top level scoped variables the top of the file) through a process called hoisting. The first block of code is equivalent to the following:

The second block has no declaration to hoist and using the undeclared x causes the reference error.

Only function scoped variable have this hoisting property.

 

Variables declared with the let or const keyword have block (also called lexical) scope. These variables are scoped to the block (set of curly braces) in which they are defined.

This includes if, for, while, function, switch/case and more!

 

 

Technically you can have scope operators in JS without anything else, but there's rarely a reason to use it.

 

Higher Order Functions

Higher order functions are functions that either take a function as it's input (examples include the Array class's map() and filter() methods) or functions that return a function as it's output.

For this topic we are interested in the latter:

The call to outer(10) returns a function inner which we then store in variable i

When we then call i(30) we can see that the function inner uses all four variables a,b,c, and d.

scope08

 

Closures

The ability to preserve inner's access to both a and b is called a closure.

closure

Specifically in this example:

The function inner closes over variables a and b (preserving our access to them even after function outer is no longer in scope)

The green region in the image above represents the closure.

Because variable unused was not used by function inner() the closure does not preserve that variable and it is subject to garbage collection.

In the underlying memory management code, each variable has a reference counter specifying how many functions depend on this variable. That number goes up if a new function uses that variable and down if a function goes out of scope. As long as a variables reference count is above zero, the garbage collection process skips over it when looking for places where memory can be recouped.

 

We are returned a refence to the function inner,

There is no way for us to directly interact with variables a, b, but they still exist, for inner to function this must be the case..

 

 

Looking at Closures

Browser gives us a tool to look at the scope of variables via the debugger keyword.

 

scope09

 

We can see access to variable b is preserved by the closure created by inner, but because variable unused is not being used by inner it is not included in this list and is a candidate for garbage collection as it falls out of scope with no closure preserving it.

 

 

v8's implementation only checks to see if the variable (unused) is ever referenced inside the enclosing function, not if it's ever actually used. Notice unused is not optimized away here.

scope10

 

The not function

Input: a function that returns a boolean

Returns: A new function that returns true for every input the original function returned false, and false for every input the original returned true.

 

 

Case 6

It looks like is_odd depends on both not and is_even, what happens if I remove their definitions?

 

scope11

 

When the not function is called a new function (...args) => !func(...args) is created.

This function maintains a reference to func using it's closure.

func points to the heap address of is_even allowing this function to continue to access it even after the identifier on the stack is nulled.

 

 

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function’s scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

 

 

Special Case - for(let)

In JavaScript these two loops are different.

In loop A:

A single memory address is allocated for i. This allows us to store a single variable which changes from 0 to 10 as the loop runs.

 

In loop B:

The first iteration of the loop creates a new variable i and initializes it to 0.

Each subsequent iteration, declares a new variable i (a new memory address is allocated) and assigns it the value of i at the end of the previous iteration (prior to running the increment statement). All future references of i will use the latest version of, but previous versions are still in memory.

 

To show this, we will borrow a function from our next topic Asynchronous Programming.

setTimeout(func, timeout)

setTimeout is a built in function that takes two inputs func and timeout. It executes func after a minimum of timeout milliseconds has elapsed.

 

This will print 10 a total of 10 times (note most browsers will group the output)

 

This will print 0 to 9.

The only way this is possible, is if all previous iterations of i are stored in different memory locations.

The closure created by the function () => console.log(i) allows us to preserve access to each i that was created along the way. This type of behavior is unique to the for(let) loop.

Generally this is the behavior that you want, but a lot of memory magic has to occur to get us there.

 

scope12

 

What are closures used for?

 

Hiding Private Members (Revealing Module Pattern)

 

 

This syntax should look very similar to class notation.

The most important part is the return statement.

An object containing multiple functions is returned back.

Each time Dog() is called name, age, and inventory, are created, effectively creating instance variables.

Each of the returned functions creates a closure that preserves that functions access to name, age, and inventory if it needs it. This ensures that those properties can be accessed, but only via the returned methods. Effectively working like public/private fields and methods.

Prior to ES6 this was the way to achieve class-like behavior.

 

Note you cannot put properties (like name and age) in the return object. (Only methods)

This is because the return statement creates a new object and if those properties are primitives, they will be passed by value. (Creating independent variables not connected to the rest of the object)

Instead getters and setter functions are required.