How JavaScript Works Behind The Scenes (Part 1)
JavaScript, the backbone of web interactivity, is in high demand. It’s crucial for creating dynamic, user-friendly web applications. JavaScript is highlighted in recent trends, where new tools and frameworks continuously shaping the development landscape. As a developer, mastering JavaScript is essential for staying competitive in today’s tech-driven world. As an experienced developer, I’ve come to appreciate the growing community of JavaScript developers today. However, not everyone may have explored the inner workings of JavaScript, so today I’m here to shed light on its behind-the-scenes operations.
Without any further, let’s get started!
I. JavaScript Engine and Runtime
What is Javascript Engine?
- A JavaScript engine is simply a computer program that executes JS code. Every browsers has its own JavaScript engine but probably the most well-known engine is Google V8.
- V8 engine powers by Google Chrome but also NodeJS.
- Any JavaScript engine always contains a call stack and a heap. A call stack is where our code is actually executed using something called execution contexts. Heap is an unstructured memory pool which stores all the objects that our application needs.
How is the code compiled to machine code?
The computer’s processor only understands 0 and 1. Therefore every single computer program automatically needs to be converted into machine code. And this can happen using compilation or interpretation
- Compilation: entire code is converted into machine code at once and written to a binary file that can be executed by any compute
- Interpretation: interpreter runs through the source code and executes it line by line
JavaScript used to be an interpreted language. However, the issue with interpreted languages is that they are much slower than compiled languages. Now, JavaScript employs a mix of compilation and interpretation known as just-in-time (JIT) compilation: the entire code is converted into machine code at once and then executed immediately. There is no portable file for execution.
JavaScript runtime in browser
- JavaScript runtime is like a large container that includes everything needed to use JavaScript. At the core of the JavaScript runtime is the JavaScript engine. However, the engine alone is not sufficient. We also need access to web APIs. The JavaScript runtime also includes a callback queue.
- Web APIs are functionalities provided to the engine but are not part of the JavaScript language. They are accessible to the engine through the browser and help with accessing data or enhancing browser functionality.”
- The callback queue consists of callback functions that are ready to be executed. The callback queue ensures that callbacks are executed in a First-In-First-Out (FIFO) method, and they are passed into the stack when it’s empty.
II. Scope & Scope chain
Define:
- Scoping: how our program’s variables are organized and accessed — “Where do variables live, or where can we access a certain variable and where not.”
- Lexical Scoping: Scoping is controlled by the placement of functions and blocks in the code.
- Scope: The space or environment in which a certain variable is declared.
- Scope of a Variable: The region of our code where a certain variable can be accessed.
Conclusion: Scope is the current context of execution in which values are “visible” or can be referenced. If a variable is not in the current scope, it will not be available for use. Scopes can also be layered in a hierarchy so that child scopes have access to parent scopes, but not vice versa.
JavaScript has the following kinds of scopes:
- Global scope: Outside of any function or block, it can be accessible everywhere.
- Function scope: Variables declared inside a function scope are only accessible inside that function. This is also called local scope.
- Block scope(ES6): block means everything in curly braces such as the block of an if statement or a for loop. Variables declared inside a block are only accessible inside that block and not outside of it. However, This only applies to let and const variables. If declare variables using var in this block then that var would actually still be accessible outside of the block and would be scoped to the current function or to the global scope. Function are also block scoped (only in strict mode).
Example:
III. Hoisting
Define:
Makes some type of variable accessible and usable in a code before they are actually declared. It’s often simplified as ‘variables being lifted to the top of their scope. However, behind the scenes, the process is different from this common description. The code is scanned for variable declarations before it is executed, and for each variable, a new property is created in the variable environment object. That’s how hoisting really works. Now, hoisting does not work the same way for all variable types. Let’s analyze how hoisting works for function declarations, variables defined with var, variables defined with let or const, function expressions, and also arrow functions.
IV. “This” keyword and Call, Bind, Apply method
- “this” keyword/variables: a special variable that is created for every execution context (every function). It takes the value of (points to) the ‘owner’ of the function in which the ‘this’ keyword is used.
- “this” is not static. It depends on how the function is called, and its value is only assigned when the function is actually called.
- “this” does not point to the function itself, and also not the its variable environment
Example:
“this” keyword now is VietnamAirline object, but when do like that, we will get error: “Cannot read properties of undefined (reading ‘push’)”
Because now, book is a regular function, and in regular function, “this” keyword point to undefined (in strict mode) or point to window.
So to do it work, we have to tell JavaScript explicitly or manually what this this keyword should look like? and to do it, we use the Call/Apply or Bind method.
- Call is a function that helps you change the context of the invoking function. In layperson’s terms, it helps you replace the value of this inside a function with whatever value you want.
- Apply: Do the same things with calls, but arguments are an array.
We can also do it with Call method
- Bind : bind does not immediately call the function. It helps you create another function that you can execute later.
V. Primitive type and reference type
In JavaScript, data types are split into two categories: primitive data types and reference data types. Primitive data types include number, string, boolean, undefined, null, symbol, and bigint. Reference types encompass everything else, such as object literals, arrays, functions, and more.
Remember the JS engine. The engine has two components: the call stack where functions are executed and a heap where objects are stored in memory. Reference types get stored in the memory heap, while primitive types are stored in the call stack.
Example:
VI. Closures
Define: Closures gives a function access to all the variables of its parent function, even after that parent function has returned. The function keep a reference to it outer scope, which preservers the scope chain throughout time.
In the above example, the parent function secureBooking has completed its execution and returned. However, the child function, named booker, maintains a persistent reference to the passengerCount variable. Upon each execution off booker, the passengerCount is incremented, even though the parent function has already concluded its execution.
This behavior exemplifies a core concept in JavaScript known as closures. Closures empower the inner function (booker in this case) to retain access to the lexical scope of its outer function (secureBooking), ensuring continued access to variables such as passengerCount beyond the lifetime of the outer function.
Here’s a slightly more interesting example makeAdder function
In this example, we have defined a function, makeAdder(x), that takes a single argument x and returns a new function. The function it returns takes a single argument y, and returns the sum of x and y.
In essence, makeAdder is a function factory. It creates functions that can add a specific value to their argument. In the above example, the function factory creates two new functions—one that adds five to its argument, and one that adds 10.
add5 and add10 both form closures. They share the same function body definition but store different lexical environments. In add5’s lexical environment, x is 5, while in the lexical environment for add10, x is 10.
*Source: Mozila
Notice: Closure has a higher priority than the global scope. For example, if there is a global variable with the same name as the variable in the function, then the “closure” will use the variable in the function during execution.”
Here is an example
As observed in the above code snippet, the result is 20 (10 * 2 = 20), not 38 (19 * 2 = 38). This outcome is influenced by closures, where the variable ‘a’ within function ‘g’ takes precedence over the ‘a’ in the global scope. Closures exhibit higher priority in accessing variables within their lexical scope compared to variables in the global scope.
In conclusion, understanding the inner workings of JavaScript is crucial for developers to master the language and create efficient and effective web applications. By understanding JavaScript engines, scoping, hoisting, the “this” keyword, primitive and reference types, and closures, developers can optimize their code and avoid common pitfalls. This article primarily aims to help you gain a deeper understanding of the underlying workings of JavaScript.
All rights reserved