JavaScript Engine
A JavaScript Engine is software that understands the JavaScript Language and can convert a JavaScript file into Machine Language, which a computer can understand.
Popular JavaScript Engines include :
V8 - This is the engine being used by Google Chrome and Node.js and is the fastest and most popular JavaScript Engine in the world today.
SpiderMonkey - This is the engine being used by Mozilla Firefox and was used by the first browser called Netscape.
Chakra - This is the engine being used by Microsoft Edge and Internet Explorer.
JavaScriptCore - This is Apple's own JavaScript Engine for its Safari browser.
Without a JavaScript Engine, writing a JavaScript file and feeding it to a computer, will be like talking to a computer in French, meaning that the computer will not be able to understand it.
Google created the V8 engine to solve the problem of the speed of JavaScript code execution, which was especially needed at the time for their Google Maps Application. It is written in C++.
Brendan Eich is a computer scientist, who created the first JavaScript Engine and the JavaScript language itself.
EcmaScript is the JavaScript standard. It is this standard that defines what JavaScript is, how it works, and how it should be implemented by the different engines.
Looking Inside the Engine
The engine reads the file and performs a lexical analysis of the code - it breaks down the code into tokens in order to identify their individual meaning and thus what the overall code is attempting to do. During this stage, it uses the JavaScript keywords to understand how the code is divided up.
It then takes these parsed tokens and uses them to construct an Abstract Syntax Tree. This tree is an organized arrangement of the parsed tokens in a way that makes it very clear to the Engine, what the code consists of.
This Abstract Syntax Tree then gets fed into the interpreter, which involves the activities of the profiler and the compiler. The result of this stage is the production of optimized code that the computer understands.
More on the interpreter coming soon.
The Interpreter!
Interpretation and compilation are two methods by which high-level code gets converted to machine code that computers can understand.
In the interpretation methodology, the file is read and executed line by line on the fly.
In the compilation methodology, the file is read in its entirety and converted from a high-level code to a low-level code. Then, this low-level code is executed.
For example, Babel is a JavaScript compiler that compiles more modern versions of JavaScript into an old version of JavaScript that browsers understand.
As another example, TypeScript is a superset of JavaScript, which eventually must be compiled down to JavaScript.
Inside the V8 Engine
An interpreted language is ideal for scenarios where code needs to be translated and executed immediately. This is why JavaScript had to be an interpreted language. You see when a server sends a JavaScript file to the browser, the file needs to be understood and run immediately because users are waiting to see results as fast as possible. Thus the interpreter is faster to get started.
However, the interpreter has to repeat its translation each time it transverses a new line. This means that ultimately it becomes slow.
The compiler on the other hand goes through everything first and translates into a much lower-level code. As a result of this, it often produces optimized code, as it factors in the entire context before producing an output.
Most engines tend to be both interpreted and compiled, in order to get the best of both worlds. This method is called JIT Compilation. (JUST IN TIME COMPILATION).
In the V8 Engine, after the parsing stage and the construction of the abstract tree, the Interpreter (ignition) does its job and produces Bytecode - which is a lower-level code that is not optimized and is not machine code, but is understood and can be executed by the interpreter.
Meanwhile, as the Interpreter is running, the Profiler (Monitor) keeps watch to take note of things that can be done to improve the code (types in use, repetitive code…). It passes what it notices to the JIT Compiler, which then produces optimized code. The sections of the Bytecode that can be improved are then replaced with optimized code. From then on, the optimized code is what will be used.
In other words, as the interpreter is running, the profiler and the compiler (Turbo Fan) work together to update the Bytecode for optimization.
It must be emphasized at this point that JavaScript is not exactly an interpreted language. It really depends on the implementation. Your engine can be designed around interpretation or around compilation or both.
Writing Optimized Code
The following keywords in JavaScript can be problematic and can make our JavaScript code less optimized:
eval()
Arguments
With
Delete
For in
You don't need to worry so much about what each of those keywords does. At this stage, simply keep in mind that they can be problematic and as such you should avoid using them as much as possible. This is because those keywords are examples of things that can disrupt the engine's Inline Caching and Hidden Classes.
Inline Caching is an action performed by a compiler to optimize code. The process is to evaluate the result of an expression or function that is executed repeatedly and just cache it, such that when next the expression or function is called, the result is immediately gotten and inserted without having to actually execute it.
When assigning properties to objects instantiated from the same class, always assign them in a predictable, rather than haphazard order, to prevent slower compilation time. This is because, behind the scenes, the JavaScript Engine uses hidden classes to maintain the properties of objects that belong to the same class.
If you are interested, you can read more about this on other internet sources.
Web Assembly
Let's turn our attention to Web Assembly for a bit. This is something you have probably heard of or come across somewhere, so I think I should talk about it for a bit.
Web Assembly is a binary executable code format for compiled JavaScript. The big idea of it is that rather than having to write JavaScript and interpret it directly on the browser, the JavaScript code can be compiled first into machine code. Then the machine code will be sent to the browser for execution.
This means that JavaScript will become much faster on the browser.
However, the success of this will depend on all the major players in the world of web browsers coming together to agree on a standard format.
If this happens, user experience on the web will be much richer and more qualitative. People will be able to play complex video games on the web and do other things that traditionally, you could not do on the web.
Meanwhile, it also means that all the browsers will be on the same level playing field as all browsers will be operating at the same speed. In other words, since they are all using the same machine code format, no one browser will be faster than the other.
In a competitive world where each corporation is striving for dominance, this is unlikely, yet, considering the efforts being made in this regard, it is worth keeping an eye on.
The Famous Call Stack and Memory Heap
The JavaScript Engine does a lot of things. However, the most important things it does are:
Storing and reading information
Executing the information it is reading.
In order to achieve this, two main components are required:
The Memory Heap - Where the program’s instructions are fetched from and stored.
The Call Stack - Keeping track of which lines of code have been run and what has not been run. It runs in a first-in, last-out mode.
Speaking of the Call Stack, it should be noted that a Stack Overflow, happens when function calls keep being pushed onto the call stack until there is no memory space available to hold a new function call. It is in instances like this that you get the error that says: "Maximum call stack size exceeded".
Recursion, when not done properly, is an easy way to cause a Stack Overflow
Recursion is when a function calls itself.
Garbage Collection and Memory Leaks
JavaScript is a Garbage Collected Language. This means that JavaScript frees up memory space when the variable it is holding there, is no longer in use.
JavaScript uses the Mark and Sweep Algorithm to implement Garbage Collection. Essentially, it marks what it needs and sweeps what it doesn’t.
Memory leaks occur when memory spaces contain values that are no longer needed but are not freed up.
Ways to reduce, if possible eliminate memory leaks:
Limit the use of global variables - global variables will always be in memory even when they are not being used.
Limit the use of event listeners - the event being listened for is always in memory even when no action is being triggered.
Using the setInterval function with referenced data - data that should have been cleared since they are no longer in use anywhere else, will remain in memory if they are being referenced inside of a setInterval function.
The Single Threaded Engine, The JavaScript Runtime, and Node.js
I bet you have heard people say: "JavaScript is single-threaded." Well, that is correct. However, what does this mean?
This means that the JavaScript Engine has only one call stack. Thus, instructions run one at a time. The first instruction runs before the second, and the second runs before the third... As a result of this, JavaScript is Synchronous.
In addition to the JavaScript Engine, all web browsers have a JavaScript Runtime. The JS Runtime, comes with a set of Web APIs that does some work in the background, while the JS Engine is running synchronously.
Some of these web APIs include:
DOM
fetch()
setTimeout()
setInterval()
In short, the window object is the complete set of web APIs that the browser provides. These web APIs allow us to do a wide variety of actions asynchronously in the background such that the JS Engine does not have to do all the work by itself synchronously.
Please understand this. When the JS Engine is interpreting and executing a program line by line and it encounters an asynchronous operation such as making an HTTP request or having to interact with the DOM, it delegates this task to the browser's JavaScript Runtime which utilizes its web APIs to perform this action in the background while the JS Engine keeps on running through the rest of the code.
The runtime also has the event loop which keeps track of events that are happening asynchronously, such that when a result has been returned, it stores the result on the callback queue and notifies the call stack. Once the call stack is free, it can take on these pending tasks, so that time is not wasted and nothing is left undone.
Node.js is a JavaScript Runtime.
It is built on top of the Chrome V8 Engine and it adds Libuv to handle its Operating System and Asynchronous Operations.
It is written in C++.
It does not have the Web Window API. It, however, has its own Global API.
With Node.js, JavaScript can be run outside of the browser and now, also but not limited to the server.
Episode One Conclusion
Thank you for going through this journey with me and for reading to the end. Please stay tuned for more to come. If you have any questions or feedback feel free to drop comments for me.