
December 11, 2023
JavaScript: How JavaScript Works Behind the Scenes
An High-level Overview Of JavaScript
As we already know that, JavaScript is a high-level, object-oriented, multi-paradigm programming language
In this section, we are going to see following topics related JavaScript, which are,
High-level
Every program that runs on our computer needs some hardware resources, such as memory and the CPU to do it's work. There are low-level languages such as C, where you have to manually manage these resources. For example, asking computer for memory to create a new variable. On the other side, High-Level languages such as JavaScript, where we don't have to manage resources at all, because these languages have abstraction that take all of these work away from us.
Garbage-collected
This is one of the powerful tools that takes memory management away from us. It is basically an algorithm inside the JavaScript engine, which automatically removes old, unused objects from the computer memory. So, we don't have to do it manually in our code.
Interpreted or just-in-time compiled
The computer's processor only understands zeros and ones. Ultimately, every single program needs to be written in zeros and ones, called machine code. Since, that's not really practical to write, we simply write human-readable JavaScript code and this code needs to be translated to machine code. This process is called either compiling or interpreting.
Multi-paradigm
One thing makes JavaScript so popular is that, it is a multi-paradigm language. In programming, a paradigm is an approach of structuring our code, which will ultimately direct the coding style and technique in a project that uses a certain paradigm. There are three popular paradigm which are procedural, object-oriented(OOP) and functional programming(FP). The procedural programming is what we have been doing so far, which is basically just organising the code in a very linear way. It's our choice to use whatever paradigm we want.
Prototype-based object-oriented
Firstly, almost everything in JavaScript is an Object, except for primitive values. For example, arrays are also object. Have you ever wondered why we can create an array and then use the push method on it?. Well, it's because of prototypal inheritance. Basically, we create arrays from an blueprint, which also called the prototype. This prototype contains all the array methods and the arrays that we create, inherit methods from that blueprint(prototype).
First-class functions
They simply means that functions are treated just as regular variables so that, we can pass functions into other functions and we can even return functions from functions.
Dynamic
JavaScript is a dynamic language and dynamic means dynamically-typed. In JavaScript, we don't assign data type to variables when we define them. Instead, they only became known when the JavaScript engine executes our code. Also, the type of variables can easily be changed as we reassign variables and this is basically that dynamically-types means.
Single-threaded
JavaScript is a single-threaded because it can execute only one statement at a time because it has only one call stack and one memory heap(we will see them in details later in the article).
Non-blocking event loop concurrency model
Firstly, a concurrency model means how the JavaScript engine handles multiple tasks happening at the same time. Since JavaScript is a single-threaded, we need a way of handling multiple things happening at the same time. In computing, a thread is like a set of instructions that is executed in the computer's CPU. However, what if there is a long-running task, like fetching data from a remote server?. Well, it sounds like that block the single thread where the code is running but we don't want that and what we want is so-called non-blocking behaviour. We can archive that by using so-called event loop which takes long-running tasks, executes them in the background and then puts them back in the main thread once they are finished and that is called non-blocking event loop concurrency model.
The JavaScript Engine and Runtime
JavaScript engine is simply a computer program that executes JavaScript code. Now, every browser has its own JavaScript engine, but probably the most well known engine is Google's V-8. The V-8 engine powers Google Chrome, but also Node.js, which is JavaScript runtime, that we can use to build server side applications with JavaScript.
JavaScript Engine
Any JavaScript engine always contains a call stack and a memory heap. The call stack is where our code is actually executed using something called execution contexts. Then the heap is an unstructured memory pool which stores all the objects that our application needs.
Now the question is how the code is compiled to machine code so that, it actually can be executed afterwards?
Compilation Vs Interpretation
As we know already that, the computer's processer only understands zeros and ones. Therefore, every single computer program ultimately needs to be converted into the machine code and this process can be done using compilation or interpretation.
In compilation, the entire source code is converted into machine code at once and this machine code is then written into a portable file(binary file) that can be executed on any computer. So, we have two different steps in this process.
JavaScript used to be a purely interpreted language, but the problem with interpreted languages is that, they are much slower than compiled languages. However, the modern JavaScript engine uses a mix between compilation and interpretation which is called just-in-time compilation. This approach is basically compiles the entire code into machine code at once and then executes it right away. So, we still have the two steps of regular compilation but there is no portable file to execute and execution happens immediately after a compilation. This is really a lot faster than just executing code line by line
So, let's now understand how this work when a piece of JavaScript code enters the engine.
step1 :- Parse the code, which is essentially means to read the code. During the parsing process, the code is parsed into a data structure called the Abstract Syntax Tree(AST). This works by first splitting up each line of code into pieces that are meaningful to the language like the const or function keywords. Then storing all these pieces into the tree in a structured way. This steps also checks if there are any syntax errors and the resulting tree will later be used to generate the machine code. For example, let's consider the following code,
const x = 23
AST for this code will look likes this,
NOTE: click here see the AST of any piece of JavaScript Code
We actually don't need to know what an AST looks like. It is important to know that, this tree has nothing to do with the DOM tree. an AST is just a representation of our entire code inside the engine.
step 2 :- This step is compilation which takes the generated AST and compiles it into machine code
step 3 :- The machine code then gets executed right away, because modern JavaScript engine uses just-in-time compilation and execution happens in the JavaScript engine call stack( we will see in the next section).
The modern JavaScript engines creates a very unoptimized version of machine code in the beginning so that, it can start executing as fast as possible. Then in the background, this code is being optimised and recompiled during the already running program execution. After the each optimization, the unoptimized code is simply swept for the new more optimized code without ever stopping execution. This process makes modern engines such as V-8 so fast.
JavaScript Runtime
We can imagine a JavaScript runtime as a big box or a big container, which includes all the things that we need in order to use JavaScript, for example, the browser. The heart of any JavaScript runtime is always a JavaScript engine because without an engine there is no runtime and there is no JavaScript at all. A typical JavaScript runtime also includes callback queue. This is a data structure that contains all the callback functions that ready to be executed.
For example, we attach event handler functions to DOM elements like, a button to react for certain events and these event handler functions are also called callback functions. As the event happens, let's say a click, the callback function will be called. The first thing that actually happens after the button click is that, the callback function is put into the callback queue. Then when the stack is empty, the callback function is passed to the stack so that, it can be executed and this happens by the event loop. So, basically the event loop takes callback functions from the callback queue and puts them in the call stack so that, they can be executed.
Execution Contexts and the Call Stack
We already know that JavaScript code execution will happen in a call stack in the engine. Let's see them in details in this section.
Let's say that, our code was just finished compiling and ready to be executed. What happen next is that a global execution context is created for the top-level code. A top-level code is actually a code that is not inside any function. So, in the beginning, only the code that is outside of functions will be executed and the functions should be executed when they are called. Let's consider following example,
const name = "mike"
const first () => {
let a = 1
const b = second(7, 8)
a = a + b
return a
}
second(x , y){
var c = 2
return c
}
const x = first()
The name variable declaration is clearly a top-level code and therefore it will be executed in the global execution context. Next, we have two functions which are one with function expression and the other one with function declaration and these functions will also be declared so that, they can be called later but the code inside the functions will only be executed when the functions are called.
what is execution context?
It is an environment, in which a piece of JavaScript code is executed. It's like a box that stores all the necessary information for some code to be executed such as local variables or arguments passed into a function. So, JavaScript code always runs inside an execution context. Let's imagine that, we order a pizza and usually it comes in a box. So, in this analogy, the pizza is the JavaScript code to be executed and the box is the execution context. In any JavaScript project, no matter how large it is, there is only ever one global execution context always there as the default context and it's where top-level code will execute.
Let's continue with above code. In the execution context, once the top-level code is finished, functions finally start to execute as well. For each and every function call, a new execution context will be created and containing all the information that is necessary to run that function. All these execution contexts together, make up the call stack. When all functions are done executing, the engine will keep waiting for callback functions to arrive so that, it will execute them for example, a callback function associated with a click event and remember that, it is the event loop who provides these new callback functions.
Execution context in Details
what is inside execution context?
The first thing that's inside any execution context is a variable environment. In this environment, all our variables and function declarations are stored. There is also a special arguments object which contains all the arguments that we passed into the function that the current execution context belongs to, because each function gets it's own execution context as soon as the function is called.
A function can also access the variable outside of the function because of the scope chain(We will see them in details in the next section). The scope chain consists of references to variables that are located outside of the current function and to keep track of these scope chain, it is stored in each execution context.
Finally, each context also gets a special variable call this keyword.
So, variable environment, scope chain and this keyword is generated in a creation phase, which happens right before execution.
NOTE:- the execution contexts belonging to arrow functions, don't have arguments object and this keyword. Instead, they can use the arguments object and the this keyword from their closest regular function parent.
For example, let's actually try to simulate the creation phase for the code given above.
As you can see that, in global context, value of x is marked as unknown, because this value is the result of the first function that we didn't run yet. In the environment of first function, the value of b is unknown, because it requires a function call in order to become known. Since, first() function is an array function, it does not have arguments object. Finally, in the variable environment of the second() function, it has arguments object as it is not an arrow function.
Now imagine, there are 100s of execution contexts for hundreds of functions. How will the engine keep track of the order in which functions are called? and how will it know where it currently is in the execution?. Well, that's where the call stack finally comes in.
The call stack
It is basically a place where execution contexts get stacked on top of each other. in order to keep track of where we are in the program execution. So, the execution context that on the top of the stack, is the one that is currently running and when it's finished running, it will be removed from the stack, and execution will go back to the previous execution context.
To demonstrate how the call stack works, let's walk through the code given above.
Once the code is compiled, top-level code will start execution. A global execution context will be created for the top-level codes and this is where all the code outside of any function will be executed.
As you can see the call stack, global context is now at the top of the stack, it is the one where the code is currently being executed. In this execution, there is a simple variable declaration called name, first() and second() functions are declared and in the last line we declare the x variable with the value that is going to be returned from calling the first() function. When the first() function is called, it get's its own execution context so that it can run the code that's inside the body. So, now our call stack will look like this,
Now first() is on the top of the call stack so, it will now start executing it's function body where we have a simple variable declaration called a, In the next line, we have another function call which will be called next. As we guessed, a new execution context will be created and put it on top of the call stack. After that, the calll stack will look like this.
Since, we are running the second() function, the first() function stopped at this point and it will continue as soon as the second() function returns. It has to work in this way as JavaScript has only one thread of execution. Let's continue with executing second() function, where it has simple variable declaring. In the next line, we have a return statement meaning that function will finished it's execution. So, the second() function will be popped off the stack and disappear from the computer's memory. After that, the call stack will looks like this,
What happens next is that, the previous execution context, will now be back to being the active execution context again which is first().By now, we can start to see how the call stack really keeps track of the order of execution. Without the call stack, how would the engine know which function was being executed before?. Back in the first function execution, where we have the calculation and then finally this first() function also returns. Same as before, the current execution context gets popped off the stack and the previous context is now the current context where the code is executed. After that, the call stack will looks like this,
Scope and the Scope Chain
Each execution context has a variable environment, a scope chain and a this keyword. In this section we are going to see about scope chain, why are so important? and how they work?.
What is Scoping?
Scoping controls how the program's variables are organised and accessed by the JavaScript engine. So basically scoping asks the question, where do variables live? or where we can access a certain variable and where not?. JavaScript has something called Lexical Scoping, meaning that Scoping is controlled by the placement of functions and blocks in the code. For example, a functions that is written inside another function has access to the variables of the parent function.
A scope is a space or environment(place), in which a certain variable is declared. There are three different scopes which are global scope, function scope and block scope. A scope of a variable is the entire region of our code where a certain variable can be accessed.
Types of Scope
Global Scope
const name = "Mike"
const age = 45
const year = 2023
This scope is for the variables that are declared outside of any functions or block. These variables will be accessible everywhere( in all functions and all blocks) in the program.
Function Scope
function calcAge(birthYear){
const curYear = 2023
const age = now - birthYear
return age;
}
console.log(now);//ReferenceError: now is not defined
Each and every function creates a scope called function scope, where the variables declared inside the function are only accessible inside that function, but not outside. This is also called a local scope, because local variables of the function are not accessible outside of the function.
As you can see the calcAge() function, variable now is accessible inside the function, but if we try to log it to the console outside of the function, we get a reference error, that's because JavaScript is trying to find the now variable in the global scope and it could not find out.
Block Scope(ES6)
if(mark > 45){
const status = "passed"
console.log(`The student is ${status}`)
}
console.log(status);//ReferenceError
Before ES6, only functions used to create scopes in JavaScript but starting in ES6, blocks({}) also creates scopes. With block scope, variables that are defined between curly braces such as the block of an if statement or a for loop. So just like with functions, variables declared inside a block are only accessible inside that block and not outside of it. However, block scopes only apply to the variables declared with let or const variables, because let and const variables are restricted to the block, where they were created. However, if we declared a variable using var inside the block, that variable would still be accessible outside of the block, and would be scoped to the current function or global scope. Just like with let and const variables, functions declared inside a block are only accessible inside the block.
let's understand these concepts in details with example,
const myName = "Mike"
function first(){
const age = 30
if(age >= 30){
const decade = 3
var millenial = true
}
function second(){
const job = "teacher"
console.log(`${myName} is a ${age} years old ${job}`);
//Mike is a 30 years old teacher
}
second()
}
first();
Let's consider only variables declaration, the myName variable is the only variable declared in the global scope. Inside the global scope, we have a scope for the first() function, because each function creates it's own scope. Inside the first() scope, the age variable declared in the first() scope and also we have second() function, which will also create its own scope, containing the job variable.
As you can see that, we have a nested structure of scopes with one scope inside other, because we need myName variable and the age variable, which were both not declared inside the second() scope. As we already know that, every scope always has access to all the variables from all it's outer scopes(parent scopes). This means that the first() scope can access variable that are in the global scope(parent scope). Similarly, the second() scope will then also be able to access the myName variable from the global scope, because it has access to the variables from the first() scope.
This is how the scope chain works. In other words, If one scope needs to use a certain variable, but cannot find it in the current scope, it will look up in the scope chain and see if it can find a variable on one of the parent scopes. If it can, it will then use that variable and it can't, then there will be an error. This process is called variable lookup. In the scope chain, every scope always has access to all the variables from all its outer scopes.
NOTE:- variables are not copied from one scope to another. Instead, scopes simply look up in the scope chain until they find a variable that they need and then they use it. However, this does not work in the other way around because a certain scope will never, ever have access to the variables of an inner scope. From the example, first() scope will never get access to the job variable. Hence, one scope can only look up in a scope chain, but it cannot look down.
With all these in place, we are able to log "Mike is a 30 years old teacher" to the console in second() function, even though the myName and age variables were not defined in the current scope.
Anyway, we still have one more scope in the example, which is created by if statement. However, these scopes only work for the ES6 variable types(let and const). So, only variable that's inside the block scope is decade variable. The millenial variable is not declared with const or let and therefore block scopes don't apply at all. Instead, the millenial variable is actually part of the first() function scope. If the millenial variable is in the first() function scope, then the second() function scope also has access to it.
The scope chain does apply to block scopes as well. Therefore, in block scope, we get access to all the variables from all it's outer scopes. From the example, block scope will have access to first() scope and global scope.
Is also important to understand that, if block scope doesn't get access to any variables from the second() function scope and the same for other way around. This is because they both are child scopes of the first() function scope. By the rule, the scope chain only work upwards, not sideways or downwards.
Difference between the scope chain and call stack?
In the call stack, one execution context for each function in the exact order, in which they were called and filled it with the variables of that functions. On the other hand, the scope chain is all about the order, in which functions are written, but it is really important to understand is that the scope chain has nothing to do with the order on which the functions were called. The scope chain does get the variable environments from the execution context but the order of function calls is not relevant to the scope chain.
Variable Environment: Hoisting and the TDZ.
Each execution context has a variable environment, a scope chain and a this keyword. In this section we are going to see about variable environment and in particular, at how variables are actually created in JavaScript.
JavaScript has a mechanism called hoisting. It basically makes some types of variables accessible/usable in the code before they are actually declared in the code. Behind the scenes, the code is actually scanned for variable declarations before it is executed and this happens during the creation phase of the execution context and for each variable that is found in the code, a new property is created in a variable environment object and that's how hoisting really works.
However, hoisting does not work the same for all variable types. So, let's analyse the way hoisting works for function declarations, variables defined with var, let and const and function expression.
Functions declarations are hoisted and the initial value in the variable environment is set to the actual function. In practice, what this means is that, we can use function declarations before they are actually declared in the code, because they are stored in the variable environment object. However, in strict mode, function declarations are block scoped.
Variables declared with var are also hoisted. If we try to access a var variable before it's declared in a code, we don't get the declared value, but we get undefined.
Variables declared with let and const variables are not hoisted. Technically, they are actually hoisted, but their value is basically set to an <uninitialized>. So, there is no value to work with at all and it is as if hoisting was not happening at all. Instead, we say that these variables are placed in a Temporal Dead Zone(TDZ), which makes it so that, we can't access the variables between the beginning of the scope and a place where the variables are declared. So, if we attempt to use a let or const variable before it's declared, we get an error.
Function expressions and arrows are simply variables so, they behave the exact same way as variables(var, let and const) in regards to hoisting. This means that, a function expression or arrow function created with var is hoisted to undefined. But if created with let or const, it's not usable before it's declared in a code because of TDZ.
Temporal Dead Zone(TDZ)
Each and every let and const variable get their own TDZ, that starts at the beginning of the scope until the line where it is defined. The variable is only safe to use after the TDZ. The main reason that the TDZ was introduced in ES6 to make it easier to avoid and catch errors, because using a variable that is set to undefined before it's actually declared can cause serious bugs. The base way to avoid it is by simply getting an error when we attempt to do so. That's what a TDZ does.
Let's consider following example,
const myName = "Mike"
if(myName === "Mike"){
console.log(`${myName} is a ${job}`);
const age = 2037 - 1989
console.log(age);
const job = "teacher";
console.log(x);
}
In this example, we are going to look at the job variable. It is a const so, it's scoped only to this if block and it is going to be accessible starting from the line where it's defined, because of TMZ for the job variable. Since, the job variable is used before it is declared, at this stage the job variable is in the TDZ, where it is still initialized, but the engine knows that, it will eventually be initialized, because it already read the code beforehand and set the job variable in the variable environment to <uninitialized>. Then when the execution reaches the line where the variable is declared, it is removed from the TDZ.
The This Keyword
The this keyword is a special variable that is created for every execution context(any function) and takes the value of the owner of the function, in which the this keyword is used. We can also say that, it points to the owner of the function. It's very important to understand that, the value of the this keyword is not static and it depends on how the function is called, and it's value is only assigned when the function is called.
For example, if we set X to 5, then X will always just be 5, but this keyword depends on the way, in which a function is called.
Let's analyse four different ways, in which functions can be called.
- Call a function as a method. So, as a function attached to an object and when we call a method, this keyword simply points to the object that is calling the method. For example,
const person = {
name: "Mike",
year:2000,
calcAge:function(){
return 2037 - this.year
}
}
console.log(person.calcAge());//37
- call a function as normal functions. In this case, the this keyword will be undefined as it is not a method or not attached to any object. However, this is only valid for strict mode. So if we are not in strict mode, this keyword will actually point to the global object, which in case of the browser is the window object.
- array function and it is not exactly a way of calling functions. Arrow functions don't get their own this keyword, instead, if we use the this variable in an arrow function, it will simply be the this keyword of the surrounding function(parent function). This is called the lexical this keyword, because it simply gets picked up from the outer lexical scope.
- function is called as an event listener, then the this keyword will always point to the DOM element that the handler function is attached to.
- call a function using new keyword, the call(), apply() and bind() methods.
Also, this keyword will never point to the function, in which we are using it and not to the variable environment of the function.
Regular functions VS Arrow functions
We will see some details of the this keyword related to regular function and arrow functions. This way we can learn when we should use and avoid each of them.
Let's consider following example,
const person = {
name: "Mike",
year:2000,
calcAge:function(){
return 2037 - this.year
},
greet: () => {
return `Hey ${this.name}`;
}
}
console.log(person.calcAge());//37
console.log(person.greet());//Hey undefined
As you can see that, greet() function used array function and when we tried to use this keyword, it returned undefined but calcAge() not. This is because, arrow function does not get its own this keyword, it will simply use the this keyword from its surrounding. In other words, it's parent's this keyword. As person object is in the global scope, greet() will use this keyword from the global scope and there is no firstName variable in the global scope. This behaviour can become pretty dangerous, in case we use var to declare variables in the global scope, because the variables declared with var, actually create properties on global object. For example,
var name = "John"
const person = {
name: "Mike",
year:2000,
calcAge:function(){
return 2037 - this.year
},
greet: () => {
return `Hey ${this.name}`;
}
}
console.log(person.calcAge());//37
console.log(person.greet());//Hey John
As you can see that greet() function returns "Hey John". This is because, inside of greet() function, the this keyword is window(global object), where name variable with var has already been created. So we should not use an arrow function as a method.
Lets consider following example,
var name = "John"
const person = {
name: "Mike",
year:2000,
calcAge:function(){
const printYear = function(){
console.log(this.year);
}
printYear();//undefined
return 2037 - this.year
},
greet: () => {
return `Hey ${this.name}`;
}
}
console.log(person.calcAge());//37
As you can see the that, when we called the calcAge() function, which is actually a regular function, printYear() function will also be called and it is also a regular function. Inside of the printYear(), we are using the this keyword and it returns the undefined of this.year. So, this means that, this keyword is undefined inside the printYear() function, because inside a regular function call, this keyword must be undefined. So, this is just as if this function was outside of the person object.
NOTE:- It's clear rule that, a regular function call has the this keyword set to undefined.
However, there are two solutions to solve the problems.
The first solution is to use an extra variable outside of the function and assign that variable to this keyword and simply use that variable inside the printYear() function to access the person object. For example,
var name = "John"
const person = {
name: "Mike",
year:2000,
calcAge:function(){
let self = this;
const printYear = function(){
console.log(self.year);
}
printYear();//2000
return 2037 - this.year
},
greet: () => {
return `Hey ${this.name}`;
}
}
console.log(person.calcAge());//37
The second solution is to use arrow function, because the arrow function does not have its own this keyword and it will simply use this keyword of its parent. In this case, that will be the calcAge() function.
var name = "John"
const person = {
name: "Mike",
year:2000,
calcAge:function(){
const printYear = () => {
console.log(this.year);
}
printYear();//2000
return 2037 - this.year
},
greet: () => {
return `Hey ${this.name}`;
}
}
console.log(person.calcAge());//37
NOTE:- Arrow function inherits the this keyword from the parent scope.
The Argument Keyword
In addition to the this keyword, functions also get access to argument keyword but it is only available in regular functions and not in the arrow functions.
For example,
const addExpression = function(a , b){
console.log(arguments)
console.log("first argument : "+arguments[0])
console.log("second argument : "+arguments[1])
return a + b;
}
console.log(addExpression(2,4));
Output:
[Arguments] { '0': 2, '1': 4 }
first argument : 2
second argument : 4
6
We can see that, argument keyword has an array of parameters( 2 and 4) that we passed in. This can be useful when we need a function to accept more parameters than we actually expected.
Primitives VS. Objects(Primitive Vs. Reference Types)
In this section, we are going to see the big difference between the way primitive types and objects are stored in memory.
Usually we call primitives as primitive types and objects as reference types, because of the different way, in which they are stored in memory. JavaScript engine has two components which are the call stack where functions are executed and the heap where the objects(reference types) are stored in memory. On the other hand, primitives(primitive types) are stored in the call stack, which means that primitive types are stored in the execution contexts, in which they are declared.
Let's start by looking at the primitive values example,
let age = 30;
let oldAge = age;
age = 31
console.log(age);//31
console.log(oldAge);//30
As you can see that, we changed the age variable to 31, but the oldAge variable is still 30. This is because we set the oldAge variable before we change the age variable to 31.
When we declare age variable, JavaScript will create a unique identifier with the variable name and then a piece of memory will be allocated with the certain address where the value(30) would be stored, 0001 in this example. It is important to know that, the identifier actually points to the address and not to the value itself. So, when we say that age variable equal to 30, in fact, we can say that age is equals to the memory address 0001, which holds the value of 30. In the next line, we declare oldAge to be equal to age. So, the oldAge will simply points to the same memory address as the age variable. Now in the next line, when we set the age variable to 31, a new piece of memory allocated and the age identifier simply points to the new address which holds the value of 31, 0002 in this example. This is why both age and oldAge variables don't returns the same value.
With reference value, things work a bit differently. Let's consider following example,
const person = {
name: "Mike",
age:30
}
const person1 = person;
person.age = 27;
console.log(person);//{ name: 'Mike', age: 27 }
console.log(person1);//{ name: 'Mike', age: 27 }
When a person object is created, it is stored in the Heap. However, person identifier does not point directly to the newly created memory address(D30F) in the heap. Instead, it will point to a new piece of memory that's created in the stack and this piece of memory will then point to the person object in the heap by using the memory address as it's value. In other words, the piece of memory in the call stack has a reference to the piece of memory in the heap, which holds the person object. So, the stack just keeps a reference to where the object is actually stored in the heap.
Now, we created a person1 variable and set to the person object. So, just like primitive values, the person1 identifier will point to the exact same memory address(which contains the reference) as the person identifier.
So, here comes the interesting part because now we are going to change age property of the person object to 31.
it's actually make sense that, when we changed the age in person object, we can also see the change in person1 object. This is because, person and person1 identifiers point to the exact same object in the memory heap.
NOTE: Even though, we defined the person variable as a constant, we can actually manipulate the object without problems because, we are not actually changing the value in the memory for the person identifier(D30F) in the stack. Instead, we changed the value in the heap. In fact, only primitive values defined with constant can not be changed. For example,
const person = {
name: "Mike",
age:30
}
const person1 = person;
person1.age = 27;
console.log(person1);//{ name: 'Mike', age: 27 }
person1 = {}//TypeError: Assignment to constant variable.
As you can see that, person and person1 both hold the same memory address reference so that's why, if we change a property on person1, it will also change on person.
However, we defined the person1 with const and const is supposed to be for constants that can't be changed. However, what actually needs to be constant is the value in the stack but in this case, we are not changing the value in the stack instead we are changing the object that is stored in the heap and this has nothing to do with const or let, because they are only about the value in the stack. However, what we can't do is to assign a completely different object to person1. if we assign empty object to person1, it will not work, because this new object will stored at a different position in memory in the stack and since it is a constant, we can't change the value in the stack
Next Article: JavaScript: Asynchronous,Promises and Async/Await
306 views