How JavaScript Works

Why understanding the fundamentals is priceless

Ionel Hindorean
Sep 2, 2019 · 19 min read
Image for post
Image for post

You’re probably wondering why anyone would bother writing a long post about core JavaScript in 2019.

It’s because I believe it’s very easy to get lost in the JS ecosystem these days without a solid understanding of the fundamentals, and virtually impossible to explore more advanced topics.

Understanding how JavaScript works makes reading and writing code easier and less frustrating and allows you to focus on the logic of your application instead of fighting with the grammar of the language.

How Does It Work?

Computers don’t understand JavaScript — browsers do.

Besides handling network requests, listening to mouse clicks, and interpreting HTML and CSS to draw pixels on the screen, the browser has a JavaScript engine built-in.

The JavaScript engine is a program, written in, let’s say, C++, which goes through all the JavaScript code, character by character, and “transforms” it into something that the computer’s CPU can understand and execute — machine code.

This happens synchronously, meaning one line at a time, and in order.

They do this because machine code is hard, and because the machine code instructions are different across CPU manufacturers.

So, they abstract all this trouble away from JavaScript developers, otherwise, web development would be much harder, less popular, and we wouldn’t have things like Medium where we can write articles like this one (and I’d be sleeping now).

The JavaScript engine can go blindly through every line of JavaScript, over and over again (see interpreter), or it can get smarter and detect things like functions that are invoked often and always produce the same result.

It can then compile these to machine code just once so that the next time it encounters it, it runs the already compiled code, which is much faster (see Just-in-time compilation).

Or, it can compile the whole thing to machine code in advance and execute that (see Compiler).

V8 is such a JavaScript engine, which Google open-sourced in 2008. In 2009, a guy named Ryan Dahl had the idea to use V8 to create Node.js, a run time environment for JavaScript outside the browser, which meant the language could also be used for server-side applications.

Function Execution Context

Like any other language, JavaScript has its own rules for functions, variables, data types, and the exact values these data types can store, where in the code they are accessible and where not, and so on.

These rules are defined by a standards organization named Ecma International and, together, they form the language specification document (you can find the latest version here).

So, when the engine transforms the JavaScript code to machine code, it needs to consider the specifications.

What if the code contains an illegal assignment, or it tries to access a variable, which, according to the specification of the language, should not be accessible from that particular part of the code?

Every time a function is invoked, it needs to figure all these things out. It achieves this by creating a wrapper, called execution context.

To be more specific and avoid confusion in the future, I will call this function execution context, because one is created every time a function is invoked. Don’t get intimidated by this term and don’t think about it too much for now, it will be detailed later.

Just remember that it determines things, such as: “Which variables are accessible in that particular function, what is the value of this inside it, which variables and functions are being declared inside it?”

Global Execution Context

But, not all JavaScript code lies inside a function (even though most of it does).

There can also be code outside of any function, at the global level, so one of the very first things that the JavaScript engine does is to create a global execution context.

This is like a function execution context and serves the same purpose at the global level, but it has some particularities.

For example, there is only one global execution context, created at the beginning of the execution, inside which all JavaScript code runs.

The global execution context creates two things, which are specific to it, even if there is no code to execute:

  • A global object. This object is the window object when JavaScript runs inside a browser. When it runs outside of it, as it does in the case of Node.js, it will be something like global. For simplicity though, I will use window in this article.
  • A special variable called this.

In the global execution context, and only there, this actually equals the global object window. It’s basically a reference to window.

Another subtle difference between the global execution context and a function execution context, is that any variables or functions declared at the global level (outside of any function), are automatically attached as properties to the window object, and implicitly to the special variable this.

Even though functions also have the special variable this, this does not happen in a function execution context.

So, if we have a global variable foo declared at the global level, the following three statements will all actually point to it. The same applies to functions.

All JavaScript built-in variables and functions are attached to the global window object: setTimeout(), localStorage, scrollTo(), Math, fetch(), etc. This is why they are accessible anywhere in the code.

Execution Stack

We know that a function execution context is created every time a function is invoked.

As even the simplest of JavaScript programs have quite a few function invocations, all these function execution contexts need to be managed somehow.

Have a look at the following example:

When the invocation of function a() is encountered, a function execution context is created as described above, and the code inside the function is executed.

When the execution of the code is finished (a return statement or the enclosing } of the function is reached), the function execution context for function a() is destroyed.

Then, the invocation of b() is encountered and the same process is repeated for function b().

But’s that’s rarely the case, even in very simple JavaScript programs. Most of the time, there will be functions that are invoked inside other functions:

In this case, a function execution context for a() is created but, right in the middle of a()’s execution, b()’s invocation is encountered.

A brand new function execution context is created for b(), but without destroying a()’s execution context, as its code is not completely executed.

This means that there are many function execution contexts at the same time. However, only one of them is actually running at any given time.

To keep track of which one is currently running, a stack is used, where the currently running function execution context is at the top of the stack.

Once it finishes executing, it will be popped from the stack, the execution for the next execution context will resume, and so on, until the execution stack is empty.

This stack is called the execution stack, represented in the image below.

Image for post
Image for post
JavaScript execution stack

When the execution stack is empty, the global execution context, which we discussed previously and which is never destroyed, becomes the currently running execution context.

Event Queue

Remember when I said that the JavaScript engine is just one component of the browser, alongside with the rendering engine or the network layer?

These components have Hooks built-in, which the engine uses to communicate with to initiate network requests, draw pixels on the screen, or listen to mouse clicks.

When you use something like fetch in JavaScript to do an HTTP request, the engine will actually communicate that to the network layer. Whenever the response for the request comes, the network layer will pass it back to the JavaScript engine.

But this can take seconds, what does the JavaScript engine do while the request is in progress?

Simply stop executing any code until the response comes? Continue executing the rest of the code, and, whenever the response comes, stop everything and execute its callback? And when the callback finishes, resume execution wherever it left off?

None of the above, even though the first could be achieved by using await.

In multi-threaded languages, this could be handled by having one thread for executing the code in the currently running execution context, and another one to execute the callback for the event. But this is not possible with JavaScript as it is single-threaded.

To understand how this actually works, let’s consider the a() and b() functions we looked at previously, but add a click handler, and an HTTP request handler.

Any event that the JavaScript engine receives from the other components of the browser, like a mouse click or a network response, will not be handled immediately.

The JavaScript engine might be busy executing code at that point, so it will instead place the event in a queue, called the event queue.

Image for post
Image for post
JavaScript Event Queue

We already talked about the execution stack, and how the currently running function execution context is popped from the stack once the code in the corresponding function finishes executing.

Then, the next execution context resumes execution until it finishes, and so on, until the stack is empty, and the global execution context becomes the currently running execution context.

While there is code to execute on the execution stack, the events in the event queue are ignored, as the engine is busy executing the code on the stack.

Only when it finishes, and the execution stack is empty, will the JavaScript Engine handle the next event in the event queue (if there is one, of course), and will invoke its handler.

As this handler is a JavaScript function, it will be handled just like a() and b() were handled, meaning that a function execution context is created and pushed onto the execution stack.

If that handler, in turn, invokes another function, then another function execution context is created and pushed on top of the stack, and so on.

The JavaScript engine will check the event queue again for new events, only when the execution stack is empty again.

The same applies to keyboard and mouse events. When the mouse is clicked, the JavaScript engine will get a click event, place it in the event queue, and only execute its handler once the execution stack is empty.

You can easily see this in action by copy-pasting the following code into your browser console:

The while loop just keeps the engine busy for five seconds, don’t worry too much about it. Start clicking anywhere on the document within those five seconds and you will see nothing is logged to the console.

When the five seconds pass and the execution stack is empty, the handler for the first click is invoked.

As this is a function, a function execution context is created, pushed to the stack, executed, and popped from the stack. Then, the handler for the second click is invoked, and so on.

Actually, the same thing happens for setTimeout() (and setInterval()). The handler you provide to setTimeout() is actually placed in the event queue.

This means that, if you set the timeout to 0 but there is code to execute on the execution stack, the handler for the setTimeout() will only be invoked when the stack is empty, which can be many milliseconds later.

This is one of the reasons why setTimeout() and setInterval() are not really precise. Copy-paste the next gist into your browser console if you won’t take my word for it.

Note: The code that is placed into the event queue is referred to as asynchronous. Whether or not that’s a good term is a different topic but that’s what people call it, so I guess you have to get used to it.

Function Execution Context Steps

Now that we are familiar with the execution lifecycle of a JavaScript program, let’s dive a little bit more into how exactly a function execution context is created.

It happens in two steps: the creation step and the execution step.

The creation step “sets things up” so that the code can be executed, and the execution step actually executes it.

Two things happen in the creation step that are very important:

  • The scope is determined.
  • The value of this is determined (I will assume you are already familiar with the this keyword in JavaScript).

Each of these is detailed in the next two corresponding sections.

Scope and Scope Chain

The scope consists of the variables and functions that are accessible in a given function, even though they were not declared in the function itself.

JavaScript has lexical scope, which means that the scope is determined based on where a function is declared in the code.

When reaching console.log(foo) above, the JavaScript engine will first check if there is a variable foo in b()’s execution context’s scope.

As there isn’t one declared, it will go to the “parent” execution context, which is a()’s execution context, simply because b() is declared inside a(). On this execution context’s scope it finds foo, and prints its value.

If we extract b() outside a(), like this:

A ReferenceError will be thrown, even though the only difference between the two is the place where b() is declared.

b()’s “parent” scope is now the global execution context’s scope, because it is declared at the global level, outside of any function, and there is no variable foo there.

I can see why this may be confusing because, if you look at the execution stack, it looks like this:

Image for post
Image for post
JavaScript execution stack

So, it’s easy to assume that the “parent” execution context is the next one in the stack, below the current one. However, this is not true.

In the first example, a()‘s execution context is indeed b()’s “parent” execution context. Not because a() happens to be the next item in the execution stack, just below b(), but simply because b() is declared inside a().

In the second example, the execution stack looks the same, but this time b()’s “parent” execution context is the global execution context, because b() is declared at the global level.

Just remember: it doesn’t matter where the function is invoked, it matters where it is declared.

But, what happens if it can’t find the variable in the “parent” execution context’s scope either?

In this case, it will try and find it in the next “parent” execution context’s scope, which is determined in the exact same way.

If it’s not there either, it will try the next one, and so on, until eventually, it reaches the global execution context’s scope. If it can’t find it there either, it will throw a ReferenceError.

This is called the scope chain, and it’s exactly what happens in the following example:

It first tries to find foo in c()’s execution context’s scope, then b()’s, and then, eventually, a()’s, where it finds it.

Note: Remember, it only goes from c() to b() to a() because they are declared inside the other, not because their corresponding execution contexts sit on top of the other in the execution stack.

If they would not be declared inside the other, then the “parent” execution context would be different, as explained above.

However, if there was another variable foo inside c() or b(), its value would have been logged to the console, because the engine stops “looking” for “parent” execution contexts as soon as it finds the variable.

The same applies to functions, not only variables, and the same applies to global variables, like console itself above.

It will go down (or up, depending on how you look at it) the scope chain, looking for a variable named console, and it will eventually find it in the global execution context, attached to the window object.

Note: Even though I only used the function declaration syntax in the examples above, scope and scope chain work exactly the same for arrow functions, which were introduced in ES2015 (also called ES6).


A closure provides access to an outer function’s scope, from an inner function.

But, this isn’t something new, I just described above how it is achieved through the scope chain.

What is so special about closures is that, even if the outer function’s code was executed, its execution context popped from the execution stack, and destroyed, the inner function will still have a reference to the outer function’s scope.

JavaScript closures

This is exactly what happens in the example above. b() is declared inside a(), so it can access the name variable from a()’s scope through the scope chain.

But not only does it have access to it, it also creates a closure, which means it can access it even after the parent function a() returns.

The variable c is just a reference to the inner function b(), so the last line of the code actually invokes the inner function b().

Even though this happens long after b()’s outer function, a(), returned, the inner function b() still has access to the parent function’s scope.

You can read more on how you can use closures in this article on Medium, by Eric Elliott.

The Value of this

The next thing that is determined in the creation step of an execution context, is the value of this.

I’m afraid this is not as straight-forward as the scope, because the value of this inside a function depends on how the function is invoked. And, to make it more complicated, you can “overwrite” the default behavior.

I’ll try and keep the explanation simple and to the point, you can find a more detailed article regarding this topic on MDN.

First of all, it depends on whether the function is declared using a function declaration:

Or an arrow function:

As mentioned above, the scope is determined exactly the same for both, but the value of this is not.

Arrow Functions

I’ll start with the easy one. In the case of arrow functions, the value of this is lexical, so it’s determined similarly to how the scope is determined.

The “parent” execution context is determined exactly as explained in the scope and scope chain section, depending on where the arrow function is declared.

The value of this will be the same as the value of this in the parent execution context, where, in turn, it is determined as described in this section.

We can see this in the two examples below.

The first one will log true, while the second one logs false, even though myArrowFunction is invoked in the same place in both cases. The only difference between the two is where the arrow function myArrowFunction is declared.

logs true
logs false

As the value of this inside myArrowFunction is lexical, it will be window in the first example, because it is declared at the global level, outside of any function or class.

In the second example, the value of this inside myArrowFunction will be whatever the value of this is, inside the function that wraps it.

I will go into what exactly the value is later in this section but, for now, it’s enough to notice that it is not window, as in the first example.

Remember: For arrow functions, the value of this is determined based on where the arrow function is declared, not where or how it is invoked.

Function Declarations

In this case, things are not so straight-forward and this is exactly the reason (or at least one of them) why arrow functions were introduced in ES2015, but bear with me, it will all make sense a few paragraphs later.

Besides the difference in syntax between arrow functions (const a = () => { … }) and function declarations (function a() { … }), the value of this inside each is the main difference between the two.

Unlike arrow functions, the value of this inside function declarations is not determined lexically, based on where the function is declared.

It is determined based on how the function is invoked. And there are a few ways you can invoke a function:

  • Simple invocation: myFunction()
  • Object method invocation: myObject.myFunction()
  • Constructor invocation: new myFunction()
  • DOM event handler invocation: document.addEventListener(‘click’, myFunction)

The value of this inside myFunction() is determined differently for each of these types of invocation, independent of where myFunction() is declared, so let’s go through them one by one and see how it works.

Simple invocation is simply invoking a function like in the example above: The function name alone, without any preceding characters, followed by () (with any optional arguments inside, of course).

In the case of simple invocation, the value of this inside the function is always the global this, which, in turn, points to the global window object, as described in one of the sections above.

That’s it! But remember, this is true for simple invocation only; the function name followed by (). No preceding characters.

Note: Because the value of this in a simple function invocation is actually a reference to the global window object, using this inside functions which are meant to be invoked by simple invocation is considered bad practice.

This is because any properties attached to this inside the function are actually attached to the window object and become global variables, which is bad practice.

This is why, in strict mode, the value of this in any function invoked by simple invocation is undefined, and the example above would output false.

When a property of an object has a function as its value, it is considered a method of that object, hence the term method invocation.

When this type of invocation is used, the value of this inside the function will simply point to the object on which the method is invoked, which is myObject in the example above.

Note: If the arrow function syntax would have been used, instead of the function declaration in the example above, the value of this inside that arrow function would have been the global window object.

This is because its parent execution context would have been the global execution context. The fact that it is declared inside an object does not change anything.

Another way a function can be invoked is by preceding the invocation with the new keyword as in the example below.

When invoked this way, the function will return a new object (even if it does not have a return statement), and the value of this inside the function will point to that newly created object.

The explanation is a bit simplified (more on MDN), but the point is that it will create (or construct, hence constructor) and return an object to which this will point inside the function.

Note: The same applies when using the new keyword on a class, as classes are actually special functions and have only small differences.

Note: Arrow functions cannot be used as constructors.

When invoked as a DOM event handler, the value of this inside the function will be the DOM element on which the event was placed.

Note: Notice that, in all the other types of invocation, we invoke the function ourselves.

In the case of event handlers, however, we don’t, we only pass a reference to the handler function. The JavaScript engine invokes the function and we have no control over how it will do it.

The value of this inside a function can be explicitly set to a custom value by invoking it using bind(), call(), or apply() from Function.prototype.

The example above shows how each of these work.

call() and apply() are very similar, the only difference being that with apply(), the arguments for the function are passed as an array.

While call() and apply() actually invoke the function with the value of this set to whatever you pass in as the first argument, bind() does not invoke the function.

It returns a new function instead, which is exactly like the function on which bind() was used, but with the value of this set to whatever you pass as an argument to bind().

This is why you see (5, 6) after a.bind(obj), to actually invoke the function returned by bind().

In the case of bind(), the value of this inside the returned function is permanently bound to whatever you pass as the this value (hence the name bind()).

No matter which type of invocation is used, the value of this inside the returned function will always be the one that was provided as an argument. It can only be modified again with call(), bind(), or apply().

The above paragraph is almost entirely true. There has to be an exception to the rule, of course, and that exception is the constructor invocation.

When invoking a function in this manner, by placing the new keyword before its invocation, the value of this inside the function will always be the object returned by the invocation, even if the new function was given another this with bind().

You can check this in the following example:

Here’s an example of how you would use bind() to control the value of this for the click event handler we discussed earlier:

Note: bind(), call(), and apply() cannot be used to pass a custom this value to arrow functions.

You can see now how these rules for function declarations, even though fairly simple, can cause confusion because of all the special cases, and be a source of bugs.

A small change on how a function is invoked will change the value of this inside of it. This can cause an entire chain reaction and that’s why it’s important to know these rules and how they can affect your code.

This is why the people that write the specifications for JavaScript came up with arrow functions, where the value of this is always lexical and determined exactly the same every time, regardless of how they are invoked.


I mentioned before that, when a function is invoked, the JavaScript engine will first go through the code, figure out the scope and the value of this, and identify the variables and functions declared in the body of the function.

In this first step (the creation step), these variables get a special value undefined, regardless of what actual value is being assigned to them in the code.

It is only in the second step (execution step) that they are assigned the actual value, and this happens only when the assignment line is reached.

This is why the following JavaScript code will log undefined:

In the creation step, the variable a is identified, and assigned the special value undefined.

Then, in the execution step, the line that logs a to the console is reached. undefined is logged, as this is what was set as a’s value in the previous step.

When the line, where a is assigned the value 1, is reached, a’s value will change to 1, but undefined was already logged to the console.

This effect is called hoisting, as if all the variable declarations are hoisted to the top of the code. As you can see, it’s not really what happens, but this is the term that is being used to describe it.

Note: This also happens to arrow functions, but not to function declarations.

In the creation step, functions are not being assigned the special value undefined, and the entire body of the function is placed into memory instead. That’s why a function can be invoked even before it is declared, like in the example below, and it will work.

Note: When trying to access a variable that has not been defined at all, a ReferenceError: x is not defined is thrown. So, there’s a difference between “undefined” and “not defined”, which can be a bit confusing.


I remember reading articles about hoisting, scope, closures, etc., and they all made sense when I was reading them, but then I would always run into some weird JavaScript behavior that I just couldn’t explain.

The problem was that I was always reading about each concept individually, one at a time.

So I tried understanding the bigger picture, like the JavaScript engine itself. How execution contexts are created and pushed onto the execution stack, how the event queue works, how this and scope are determined, etc.

Everything else just made sense after that. I started spotting potential issues earlier, identified the source of bugs faster, and became much more confident about my coding in general.

I hope this article does the same for you!

Better Programming

Advice for programmers.

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Ionel Hindorean

Written by

Web Engineer. Twitter: @ionelh Linkedin: Ionel Hindorean

Better Programming

Advice for programmers.

Ionel Hindorean

Written by

Web Engineer. Twitter: @ionelh Linkedin: Ionel Hindorean

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store