How JavaScript Works. Why understanding the Fundamentals is… | by Ionel Hindorean | Better Programming

Why do we need to understand the fundamentals

You might wonder why anyone would bother to write a long article about the core of JavaScript in 2019.

This is because I believe that without a solid understanding of the basics, it’s easy to get lost in the JS ecosystem, and it’s almost impossible to explore more advanced content.

Understanding how JavaScript works can make reading and writing code much easier, reducing frustration and allowing you to focus on the logic of your application instead of struggling with the language’s syntax.

How does it work?

Computers don’t understand JavaScript, browsers do.

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

A JavaScript engine is a program written in C++ that looks at all the JavaScript code word for word and “translates” it into something a computer CPU can understand and execute — machine code.

The process is synchronous, that is, they’re on a timeline, and they’re in sequence.

They do this because machine code is difficult and machine code instructions vary from CPU manufacturer to CPU manufacturer. So, they abstract all this hassle away from JavaScript developers, otherwise web development would be harder and less popular, and we wouldn’t have something like Medium that allows us to write articles like this (and I’m sleeping right now).

The JavaScript engine can machine-scan every line of JavaScript, over and over again (the interpreter), or it can get smarter and detect things like functions that are often called and always produce the same results.

It can then compile these things into machine code once, so that the next time it encounters it, it will run the code that has already been compiled, which is much faster (compile in time).

Alternatively, it can compile the whole thing into machine code ahead of time and then execute it (see compiler).

One such JavaScript engine is V8, which Google made open source in 2008. In 2009, a guy named Ryan Dahl came up with the idea of using V8 to create Node.js, which is a JavaScript runtime environment outside of the browser, meaning that the language could also be used for server-side applications.

Function execution context

Like other languages, JavaScript has its own rules about functions, variables, data types, the exact numbers that those data types can store, where in the code they can access, where they can’t, and so on.

These rules are defined as standards by an organization called Ecma International, and together they make up the language specification file (you can find the latest version here).

Therefore, when the engine converts JavaScript code into machine code, it needs to take these specifications into account.

What if the code contains an illegal assignment, or if it attempts to access a variable that, according to the language’s specifications, should not be accessed from a particular part of the code?

Every time a function is called, it needs to figure out all of these things. It does this by creating a wrapper called an “execution context”.

To be more specific and avoid future confusion, I’ll call this the function execution context, since one is created each time the function is called. Don’t be intimidated by this term. Don’t think too much about it for now, more on that later.

Just remember, it determines things like.” What variables are accessible in that particular function, what values are in it, and what variables and functions are declared in it?”

Global execution context

However, not all JavaScript code is in a function (although most of it is).

Beyond any function, there may be code at the global level, so one of the first things the JavaScript engine does is create a global execution context.

This is like a function execution context that does the same thing at the global level, but with some specificity.

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

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

  • A global object. When JavaScript is running in the browser, this object is the window object. When it operates outside the browser, as it does in Node.js, it will be a global-like object. For simplicity, however, I’ll use Window in this article.

  • A special variable called this

In the context of global execution, there is also only this, which is actually equal to the global object window. It’s basically a reference to window.

this= = =window // logs true
Copy the code

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

Although functions also have a special variable called this, this does not happen in the context of function execution.

foo; // 'bar'
window.foo; // 'bar'
this.foo; // 'bar'
(window.foo === foo && this.foo === foo && window.foo === this.foo) // true
Copy the code

All JavaScript built-in variables and functions are attached to the global window object: setTimeout(), localStorage, scrollTo(), Math, fetch(), and so on. That’s why they can be accessed anywhere in the code.

Execution stack

As we know, a function execution context is created each time a function is called.

Since even the simplest JavaScript programs have quite a few function calls, all of these function execution contexts need to be managed in some way.

Here’s an example:

function a() {
  // some code
}

function b() {
  // some code
}

a();
b();
Copy the code

When a call to function a() is encountered, a function execution context is created and the code within that function is executed, as described above.

When the execution of the code is complete (return statement or reach the enclosing function}), the execution context of function a() is destroyed.

Then, a call to b() is encountered and the same process is repeated for function b().

But this rarely happens, even in very simple JavaScript programs. In most cases, there are functions that are called within other functions:

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

a();
Copy the code

In this case, the function execution context of a() is created, but during the execution of a(), a call to b() is encountered.

A completely new function execution context is created for b(), but the execution context for a() is not broken because its code has not yet been fully executed.

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

To keep track of currently running functions, we use a stack with the currently running function execution context at the top of the stack.

Once it has finished executing, it will be ejected from the stack, and execution of the next execution context will continue, and so on, until the execution stack is empty.

This stack is called the execution stack, and it looks like this:

When the execution stack is empty, the global execution context we discussed earlier, which was never destroyed, becomes the current running execution context.

The event queue

Remember I said that the JavaScript engine is just a component of the browser, alongside the rendering engine or the network layer.

Each of these components has built-in Hooks that the engine uses to communicate to initiate network requests, draw pixels on the screen, or listen for mouse clicks.

When you make an HTTP request using something like FETCH in JavaScript, the engine actually communicates it to the network layer. Whenever a response to the request arrives, the network layer passes it back to the JavaScript engine.

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

Simply stop executing any code until a response arrives? Continue with the rest of the code, stopping everything and executing its callback every time the response arrives? When the callback is complete, continue where it left off?

None of the above, although the first one can be implemented using await.

In multithreaded languages, this can be handled by one thread executing the code in the execution environment in which it is currently running, and the other thread executing the callback of the event. But this is not possible in JavaScript because it is single-threaded.

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

function a() {
  // some code
  b();
  // some more code
}

function b() {
  // some code
}

function httpHandler() {
  // some code here
}

function clickHandler() {
  // some more code here
}

a();
Copy the code

Any events received by the JavaScript engine from other components of the browser, such as mouse clicks or network responses, are not immediately processed.

At this point, the JavaScript engine is probably busy executing code, so it will put the events in a queue called an event queue.

We’ve talked about execution stacks and how the execution context of the currently running function pops off the stack once the code in the corresponding function has finished executing.

The next execution context then resumes execution until it completes, and so on until the stack is empty and the global execution context becomes the currently running execution context.

When there is code in the execution stack to execute, events in the event queue are ignored because the engine is busy executing the code in the stack.

Only when it completes and the execution stack is empty will the JavaScript engine process the next event (if any, of course) in the event queue and call its handler.

Since this handler is a javascript t function, it is handled just like a() and b(), that is, an execution context for a function is created and pushed onto the execution stack.

If the handler in turn calls another function, the execution context for the other function is created and pushed to the top of the stack, and so on. It is only when the execution stack is empty again that the JavaScript engine checks the event queue again for new events.

The same applies to keyboard and mouse events. When the mouse is clicked, the JavaScript engine gets a click event, puts it in the event queue, and executes its handler only if the execution stack is empty.

You can easily see this process by copying the following code to your browser console:

function documentClickHandler() {
  console.log('CLICK!!! ');
}

document.addEventListener('click', documentClickHandler);

function a() {
  const fiveSecondsLater = new Date().getTime() + 5000;
  while (new Date().getTime() < fiveSecondsLater) {}
}

a();
Copy the code

The while loop only keeps the engine busy for five seconds, so don’t worry too much. Start clicking anywhere on the document during those five seconds, and you’ll see that nothing is recorded to the console.

When five seconds pass and the execution stack is empty, the handler of the first click is called.

Since this is a function, a function execution context is created, pushed to the stack, executed, and popped off the stack. The handler for the second click is then called, and so on.

In fact, the same is true for setTimeout() (and setInterval()). The handler you provide to setTimeout() is actually put in the event queue.

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

setTimeout(() = > {
  console.log('TIMEOUT HANDLER!!! ');
}, 0);

const fiveSecondsLater = new Date().getTime() + 5000;
while (new Date().getTime() < fiveSecondsLater) {}
Copy the code

Note: Code that is placed in the event queue is said to be asynchronous. Whether it’s a good term or not is another topic, but that’s what people call it, so I think you have to get used to it.

Function to perform the context step

Now that we are familiar with the execution cycle of JavaScript programs, let’s take a closer look at how function execution contexts are created.

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

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

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

  • determinescope.
  • Determine the value. I’ll assume you’re already familiar with JavaScriptthisKeywords).

These are covered in detail in the next two corresponding chapters.

How JavaScript works (part 2)