Everything you need to know about the JavaScript engine, scopes, closures, the event queue, and how to apply this knowledge.

Table of Contents

— Prerequisites

— Definitions

— The Call Stack

— — 1. Variable & Function Declarations (creation phase)

— — 2. Execution

— Scope and The Scope Chain

— — Function/Lexical Scope

— — Block Scope

— The Event Loop

— Code Examples

— — Simple Closure

— — Blocking Code

— — Defer a Function

— — Memoize with Closures

— Resources and Links

Prerequisites

You only need a basic knowledge of JavaScript. While this seems like an advanced tutorial, it’s one of the fundamental concepts of JS that you should learn early to save a lot of time debugging later.

Definitions

There will be a lot of terms used here so let’s get them out of the way early. For our purposes, we’ll use simplified definitions and go in depth later.

(JavaScript) Engine — a program that reads your code, decides if it’s valid and runs it. There is no single “JS Engine”; each browser has its own engine. i.e. Google has V8

Scope — the “area” you can access a variable from.

Lexical Scope — scope determined by where the running piece of code physically sits in your source-code. Anytime I say lexical or lexically I mean where it’s literally sitting inside your code.

Block Scope — scope created by curly braces {}

Scope Chain — a function can go up to its outer environment (lexically) to search for a variable, it can keep going until it reaches the global environment. We call this going up the scope chain.

Global — not inside a function, available anywhere.

Synchronous — one thing at a time; a “synchronous” engine only executes one line at a time. JavaScript is synchronous and executes code line by line starting at the top of the file.

Asynchronous — multiple things at a time; JS emulates async behavior via browser APIs

Event/Callback Queue — Events handled after the call stack is empty. Callback functions are pushed here when using an external browser API. It uses the queue data structure which acts as a line, first in first out.

Event Loop— the process of a browser API finishing a function call, pushing a callback function onto the callback queue, and then when the stack is clear it pushes the callback function onto the call stack.

Stack — a data structure where you can only push elements in and pop the top one off. Think of stacking a literal tower of blocks; you can’t remove the middle block. Last in, first out. (or first in last out)

Heap — where variables are stored in memory

Call Stack — the place for function calls; it’s another data structure that implements the stack data type, meaning one function can run at a time. Calling a function pushes it onto the stack and returning from a function pops it off the stack.

Execution Context — an environment created by JS when a function is added to the call stack.

Closure — when a function is created inside another function and it “remembers” the environment it was created in when called later.

Garbage Collected — when a variable in memory is deleted automatically. (as it’s no longer used, the engine gets rid of it)

Hoisting — when variable and function declarations are placed into memory but not assigned a value. Functions will work and variables will exist but are set to undefined .

this — a variable/keyword created automatically by JavaScript for each new execution context.

Great, now we have some terms defined let’s jump right into it starting with understanding the call stack.

The Call Stack

Consider this code:

stack example a-b-c

There are a few things to notice here:

Where the variables are declared ( one up top, one on bottom ) The function positions ( a calls b which is defined below it & b calls c )

What do you expect to happen when it’s executed? Does an error occur because b is declared after a or will everything work fine? What about the console.log for the variables?

Here’s the result:

Let’s break down the steps JavaScript takes.

1. Variable & Function Declarations (creation phase)

The first step is setting up space in memory for all variables and functions. Note, however, variables are not yet assigned a value except for undefined . Therefore myVar is undefined when it gets logged — it has not yet been assigned a value since the JS engine executes code line by line starting at the top.

Functions are different because they get declared and initialized in one go, meaning they can work from anywhere.

So far the code looks like this:

the global context so far

These all exist in the global context created by JS because it sits lexically (physically in your source code) in the global space.

In the global context JS also adds:

A global object ( window in a web browser or global in NodeJS) A reference to this which points to the global object

2. Execution

Next, it executes the code line by line.

myOtherVar = 10 — Inside the global context, myOtherVar is assigned to the value 10

It already created all the functions, and the next step is to execute a

a()

Every time we call a function, a new context is created for that function (repeat step 1) and is pushed onto the call stack. Here are the steps:

function a

New context created No variables or functions are declared here It creates this and points it to the global object It creates a reference to its outer lexical environment, again this is the global object. We’ll see what happens with a function inside a function later. Execute code line by line Repeat steps for b() and so on

Here’s the example visualized on the call stack:

call stack visualized

To reiterate the steps:

It creates the global context, global variables, and functions. For each function call, it creates a new context allocating memory for any variables/function declaration, a reference to its outer environment, and this . When a function returns — in the example we don't have a return statement but there’s an implicit return undefined — it gets popped off the stack and the execution context for it is garbage collected. (unless there’s a closure) When the call stack is empty, it then takes events from the event queue. (more on that later)

Scope and The Scope Chain

In the previous example, everything was globally scoped, meaning we could access it from anywhere in the code. Now, we’ll look at private scoping and how scope is defined.

Function/Lexical Scope

Consider this code:

scope & scope chain example code

It’s like the first example, but now notice:

There are variables declared both in the global scope and inside functions Function c is now declared inside function b

What do you think it will output to the console?

The result:

myOtherVar: "global otherVar"

myVar: "inside B"

Here are the steps:

Global Creation & Declaration — it creates all the functions and variables in memory along with the global object and this Execution — it reads the code line by line, assigns the variables to values, and executes function a . function a creates a new context and gets pushed onto the stack. It creates the variable myOtherVar inside the context and then calls function b . A new context for function b is pushed onto the stack. The same creation/declaration phase happens and creates another myVar variable, and function c is declared lexically inside of function b .

Remember the outer reference thing we mentioned that gets created for each new context? The outer reference depends on where a function was declared in the source code. This is where it comes into play.

function b is trying to log myOtherVar but that variable does not exist inside function b . What does it do? It *goes up the scope chain* using its outer reference. Since function b was declared globally and not inside function a it uses the global variable. Function c is called and follows the same steps. As function c has no variables of its own it goes up the scope chain using its outer reference which is function b because it was declared inside. It finds myVar inside of the context of function b and logs that.

Here’s the code again with comments:

scope & scope chain with comments

Here’s a diagram for visualizing scope:

scope diagram

Keep in mind that the outer reference only points to one thing — it’s not a two-way relationship. For example, function b can’t just jump into the context of function c and grab variables from there.

It might be better to think of it as a chain (a scope chain) that can only go in one direction.

a -> global

c -> b -> global

One thing you might notice in the above diagram is that functions are the only pieces of code that create a new scope. (besides global) However, there is another way to create new scopes, and it’s called block scoping.

Block Scope

In this next example, we have two variables and two loops that re-declare the same variables. What happens?

loops block vs function scope

The Result:

i = 10

j = 99

The first loop overwrote var i and for the unsuspecting developer, this can lead to bugs.

The second loop, however, created its own scope and own variable for each iteration. This is because it uses the let keyword which is the same as var except that it uses block scoping. Another keyword is const which is the same as let but it’s constant and can’t be changed.

Block Scope — scope created by curly braces {}

Here’s another example:

block scope example

The result:

a = 9000

b = 11

ReferenceError: blockedVar is not defined

Here are the steps:

All the usual creation phase / executing / adding to the stack stuff we covered before a is block scoped, but it’s in the function and not nested so it’s practically the same as using var in this case. There’s a literal block {} in the code and for block-scoped variables, this behaves like a function. ( notice var b is accessible outside it but const blockedVar is not ) Inside the block, it can still go up the scope chain and change a to 9000.

Using block scope makes for cleaner, safer code and you should use it wherever you can.

The Event Loop

The final part of the JS engine we’ll cover is the event loop. This is where callbacks, events, and external browser APIs come into play.

Side Note: One other thing we didn’t really talk about much is the heap, which is where variables are stored. As there’s not much practical use of knowing how X JS engine implements its data storage, we won’t be covering it here.

Our first asynchronous code example:

first async example

All this code does is log some messages to the console. It also makes use of the setTimeout function to delay one message. Knowing that JavaScript is synchronous (executes one line at a time) what do you think will happen? Why will it happen?

The Result:

Message 1

Message 3

Message 2

It logs Message 1 It calls setTimeout and then removes it from the call stack It logs Message 3 Sometime later, it logs Message 2

What happened in step 2? Where did our function go?

setTimeout is a Web API and like most web APIs, when it’s called it sends some data and a callback off to the web browser. With setTimeout we send a delay in milliseconds, 1000 and our callback function is logMessage2 . The browser handles the rest. (it counts for one second)

Meanwhile, our code continues on, not pausing, logs Message 3 and does whatever else it has to do.

Once the browser finishes waiting for one second, it passes data to our callback (none in the case of setTimeout ) and adds it to the event/callback queue. It then sits in the queue and only when the call stack is empty it gets pushed onto the stack.

first async example animated (play with this yourself)

Code Examples

The best thing you can do to become familiar with the JS engine is to play with it. So, here are some more examples to play with.

Simple Closure

In this example, we have a function that returns a function. This inner function when called later accesses its outer environment. This is called a closure.

simple closure — exponent function

Blocking Code

What happens if you were to flood the call stack? Well, you will block the callback queue because it can only add to the call stack once it’s empty.

blocking the call stack

We tried to call a function after only 250 milliseconds but since our loop blocked the stack it took two seconds. This is a common bug in JavaScript applications.

setTimeout is not guaranteed to call a function exactly after a set duration. Instead, it is better described as, “call this function after this amount of time has passed at a minimum”.

Defer a Function

What happens when you use setTimeout with a delay of zero?

defer a function with setTimeout 0

Well, you might expect it to be called immediately, however, that is not the case.

The Result:

after timeout

last log

timeout with 0 delay!

It gets pushed onto the callback queue immediately, but it still waits until the stack is clear.

Memoize with Closures

Memoization is the process of caching the result of function calls.

For example, pretend we have a function add that adds two numbers. We then call add(1, 2) and it adds them together and returns 3 We call it again with the same arguments add(1, 2) but this time instead of recalculating, it remembers that 1 + 2 is 3 so it just returns that. Memoization can speed up your code and is a nice tool to have.

We can implement a simple memoize function in JavaScript using closures.

memoize with closures in JS

The result:

cache {} false

first add call: 3

cache { '[1,2]': 3 } true

second add call 3

The first time we call add the cache object is empty, and it calls our passed in function to get the value 3. It then stores the args/value pair in the cache object.

In the second call, it finds the arguments in the cache and returns the value.

It seems insignificant or even less efficient for an add function, but for some complex computations, it could save a lot of time. This example is not meant to be a perfect memoize example but just a real-world use for closures.

Resources and Links

— — — — — — — — — — — — — — — — — — — —

Thanks for reading!

More examples and clearer explanations will be added soon. Leave any questions or feedback below.

— — — — — — — — — — — — — — — — — — — —