Skip to content

An in-depth explanation of the working of JavaScript behind the curtains

Notifications You must be signed in to change notification settings

mahessh77melo/JS-behind-the-scenes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 

Repository files navigation

Working of JavaScript behind the scenes


Features Of JavaScript :


High Level Language

JavaScript is a high level language like python. Unlike c, c which are low level languages, to run JS, a developer doesn't need have the need to manage the resources. Everything kind of happens automatically in JS and in python. As you may know, the downside is that these programs are not very optimized like the low level languages.

Garbage collection

These languages have inbuilt memory management, it removes used objects from the runtime memory and we don't need to do it manually. It's like these langagues have a cleaning guy

Just in time interpreted

It is not compiled before running, and that is why typescript is used for. JavaScript is converted into machine understandable binary code before running. All objects, functions, variable assignments happen on the go in JS.

Multi paradigm

Wait, what ???? what the heck is multi whatever! JavaScript has many methods and styles of programming.

  1. Procedural Programming

  2. Object oriented Programming

  3. Functional Programming

Many languages come under only one of those paradigms. But JS does it all. More like Lebron James.

Prototype based and object Oriented

Anything and everything in JS in an object. For example : Functions, arrays etc.

Look at the following block of code.....

let arr = [0,1,2,3]
arr.push(4)
const newArr = [7,8,9]
arr = arr.concat(...newArr)
console.log(arr)
// [0,1,2,3,4,7,8,9]

The methods concat and push are available from Array.prototype object. Any array ever created inherits this object thereby gaining access to these methods.

First class functions

All functions are treated as variables in Python and JavaScript. This gives us the luxury of passing them as variables and calling functions inside functions. A simple and popular example of this would be the function that you pass into the Event Listeners.

Look at this block of code....

const handleClick = ()=>{
    console.log('clicked')
}
document.querySelector('.div').addEventListener('click',handleClick)

Notice how the function handleClick is not called there, but instead it is just passed into the event listener and it does the rest. Another way of using it would be...

document.querySelector('.div').addEventListener('click',()=>handleClick())

here, the second argument of the event listener is just a function which in turn calls another function --> handleClick()

Dynamically interpreted

Python people may find this word familiar. In JS, dataTypes are not fixed by the compiler or whatever and we have the advantage of assigning the types of the variables on the go. Variable types like var and let in ESnext allow us to keep changing the values and datatypes.

let a='3'
a=45;
a = a '3'
console.log(a)
// 48 or '453' depending on the browser.

this example explains why many people want JS to be a strongly typed language, (compiled and then executed) so that it runs the code as we expect every single time.

This reduces numerous possible bugs. Languages like Java, c , ruby are strongly typed languages. If u want to experience strongly typed JS, then go take a look at TypeScript (TS).

Single threaded

This is a complicated one. If you want to go full on in this topic, check out something called Concurrency model . The way in which JS executes multiple tasks at a time.

By default, JavaScript is capable if running a single task at a time and thats where the name Single threaded comes from.

So what happens when a tasks runs for a considerably long time ....say 5 seconds or something, it would block the whole execution.

fetch(`${apiHeader}/movie?id=${current.id}`).then(res=>res.json())
.then(res=>{
    console.log(res.data)
}).catch(err=>{
    console.log(err.message)
})

this is something we would have come across a lot. These fetch api calls usually take a particular amount of time to finish executing which is non-negligible. What happens to our execution context meanwhile??.

The execution context pushes these type of long running functions to the background and the execution stack keeps running the other functions. So it doesn't delay anything. After the execution of the long running function in the background, it returns back to the main thread. This whole process is handled by something called an Event Loop.

Note : the name long running is kind of weird, so the correct word is Asynchronous function



The JavaScript V8 Engine :


Every browser has a runtime engine with it. Browsers can't work without that.

Browsers like Chrome, Apple Safari, Microsoft Edge use Webkit engine.

Whereas Firefox stands out and uses an engine called Gecko.

These are the engine that are used to render the web pages, but each of them has a different JavaScript engine.

Browser JS engine
Chrome Google's V8
Safari JavaScriptCore
Firefox SpiderMonkey
Opera Carakan

Google's V8 is by far the most popular because it is used by popular frameworks such as Node.js, Deno.js etc. This is because node and deno run on the machine directly. Those stand out from the traditional front-end client side JavaScript and are capable of running on the machine itself. But that's a topic for a whole other day.

All these engines mentioned above convert the human readable js code into browser readable machine code before execution. For newbies, engine is not very different from a compiler.

Every engine is split into a call stack and a heap. The call stack is where the execution stack exists and the objects and the variables are stored in the heap.

Difference between Compilation and Interpretation :-

In compilation, the whole code is converted into machine code all at one time and the execution occurs wayyy after the compilation. You might have seen two seperate shortcuts for compilation and execution in IDEs for JAVA, C etc.

In Interpretation, the whole code however, has to be converted into machine code, but the execution occurs simultaneously. As the compiler or engine reads the code line by line, it is converted into machine code just before execution.

As said earlier,dynamically interpreted languages are slow to be interpreted.

This was acceptable until JavaScript was used only to annoy the users with childlish tricks in the web and color changing buttons etc. But in modern day world where ESnext is the trend, this cannot be acceptable. That's why JS is a Just in time compiled language. The execution starts as soon as the compilation is over. Execution is not manually triggered by the user. Conclusion......

JAVASCRIPT IS AN INTERPRETED LANGUAGE

Hidden processes that occur with compilation and execution :-

  1. Parsing : The whole code is read by the engine and something called an AST is generated. This represents all the variables declared and assigned values in the code. This Abstract syntax tree is then used in the process of compilation.

  2. Optimization: The compilation generates a less optimized version of a machine code initially. But then simulateously during execution, the machine code is again converted into human code and converted back to machine code in an optimized way. This process is like a cycle.

Different engines go through this process in their own ways.



The JavaScript Runtime


Obviously, from what we know so far, the js engine is the heart of the runtime. But js requires some other things to run too.
  1. The js engine --> (call stack and the heap)

  2. WEB APIs --> (access to the DOM, fetch() function, Timers)

  3. Call-back queue --> (every callback function that should occur on a given condition)

All of the above are self explanatory. A point to be noted is that the node js runtime doesn't have access to the WEB APIs, and that's why it doesn't have access to the document.querySelector(), fetch() functions. Subsequently, it uses something called C bindings and a Thread pool. That's why it gains access to the require() method.

The call back queue is maintained by the Event Loop which is responsible for the non-blocking and concurrent behaviour of the js runtime.

Execution Context :-

During compilation, something called a global execution context is created. The top-level code is inside this context. Meaning, all the lines of code which aren't inside of a function. This is obvious because we want a function to be executed only when it is called ryt?? yup!

The variables that exist in this global context are global variables. JavaScript code always runs inside an Execution context. No matter how big a program is, it will have only one global context.

So what happens to functions, methods and callbacks?

Each have their own and new execution contexts that contain necessary information to run the code block.

All these execution contexts make up for the call stack. The callback functions are monitored and called by the EVENT LOOP.

What is inside an execution context?

  1. Variable environment

    1. let const and var declarations.

    2. functions and keywords

    3. the arguments object

  2. The scope chain

  3. the this object.

The scope chain and the this key word are created in the creation phase.

NOTE : Arrow functions don't have an arguments object and also they don't have a this keyword. Both return undefined.

The call Stack :-

Initially, the global context sits in the call stack. But things start to deviate from boring only when a function call happens. This is when the execution context of that specific function is pushed into the call stack. After the call stack finished executing the function, it return back to the global context since that was the one that was being dealt with previously.

This explains why the call stack is called the call stack. It executes the context at the top of the stack. After the execution is over and function returns something..... it is thrown out of the call stack and then again the top most context comes back into deal.

Lets complicate a bit. What happens when a function is called inside another function.

Look at this code block....

let x;
const first = () =>{
    let a = 2;
    const b = second(5,7);
    a = a b;
    return a;
}
function second(c,d){
    return c d;
}
x = first()
// x becomes 14

The global context contains the following four variables.

Name Type
x <let>
first <function>
second <function>
x <unknown>

When the first function is called, a new execution context is created and seperate, variable environment and scope chain are created for it.

Inside the first function, the second function is called. This creates a new execution context. At this point of time the call stack looks like this.....

Call Stack
second()
first()
Global Context

The first() function waits for the second function to finish and when that happens, the second() functions is popped from the call stack. Now the global context waits until the first() function returns a value. This is a simple example, but things don't change much even when the scale is increased.

TAKE-AWAY : The call stack always executes the top most context it has inside it. The call stack ensures that the order of execution never gets lost.



Scope

Scope is generally the indicator that where our variables and functions are available for access and where not. Where do our variables live.

In JavaScript , Lexical Scoping is used. That is...... scope depends on where we place our functions and variables (location).

One might ask, "What is the difference between the variable environment of the execution stack and the scope??".

Well, for functions, both are the same.

Three types of Scopes...

  1. Global Scope

    1. Outside of any function block. Top-level code.

    2. Variables declared here are accessible anywhere in the code.

  2. Function Scope

    1. Variables created here are accessible only inside the function, Not outside.

    2. Also called as local scope.

  3. Block Scope

    1. New feature in ES6.

    2. Any variable inside a code block can be accessed only inside that specific block.

    3. By code blocks, I mean... if blocks , for loop blocks etc.

    4. Since this is a new feature in ES6, block scoping only applies for const and let type variables. However, the var is much more flexible.

// Example for global scope
const x = 5;
if(true){
    console.log(x)
}
// 5 is printed to the console
var x=5;
const y=6;
const printEveryThing = ()=>{
    let z = 7;
    console.log(x,y,z);
}
printEveryThing();
// 5,6,7 are printed to the console

console.log(z)
// -- this gives Reference error

NOTE : Functions are also block scoped in 'strict mode'. Strict mode can be achieved with typing 'use strict'; in the first line of code.

One important thing to note is that, the scope chain has nothing to do with the call stack or the order in which the functions are called.

Observe the following example.......

function second(){
    const c = 3;
    third();
}
function third(){
    var d = 4;
    console.log(d c);
}
// this throws a reference error since c is not available to third() func

TAKE-AWAY : The scope chain is a one way street. The outer scope never has access to the inner scoped variables.

A scope chain is equal to adding all the environment variables of all the parent scopes.



A simple example :

Case One :-

'use strict';
function calcAge(birthYear) {
  const age = 2020 - birthYear;
  const printAge = () => {
    const output = ` ${firstName} is ${age} years old 
                        and is born in ${birthYear}`;    
    console.log(output);
  };
  printAge();
  return age;
}
const firstName = 'Mahesh';
const age = calcAge(1999);

As you may see, everything goes well and good in this function.

Because when the function scope can't find a firstName variable, it looks outside its scope until it finds one, which it does....in this case.

the output would be...

Mahesh is 21 years old and is born in 1999

Case Two :-

but, when we interchange the last two lines...

'use strict';
function calcAge(birthYear) {
  const age = 2020 - birthYear;
  const printAge = () => {
    const output = ` ${firstName} is ${age} years old and 
                     is born in ${birthYear}`;
    console.log(output);
  };
  printAge();
  return age;
}
const age = calcAge(1999);
const firstName = 'Mahesh';

This throws a reference error. Eventhough vscode will show u the value of the global variable when u hover over the firstName variable inside the printAge function, js won't like that. This is because when the calcAge function enters the execution stack, the firstName variable hasn't entered the global scope yet. Therefore, the output will be....

ReferenceError: Cannot access 'firstName' before initialization

Note : You may want to notice that I have declared the age variable (const) twice . But Js feels ok with it, that's because const and let are block scoped. And moreover, one of them is function scoped.

Since we changed topics midway, let us play with scopes...

'use strict';
function calcAge(birthYear) {
  const age = 2020 - birthYear;
  const printAge = () => {
    const output = `${firstName} is ${age} years old and
                     is born in ${birthYear}`;
    if (age > 18) {
      var adult = true;
    }
    console.log(adult ? 'You are an adult' : 'Chota bachaaa!!!');
    console.log(output);
  };
  printAge();
  return age;
}
const firstName = 'Mahesh';
let age = calcAge(1999);

As u may think, it says, You are an adult xD. JS feels okay with this behaviour. But when u try to declare adult as a const or let then JS yells at you.

Note : Starting from ES6, functions are also treated as block scoped.

Case Three :

'use strict';
function calcAge(birthYear) {
  const age = 2020 - birthYear;
  const printAge = () => {
    const firstName = 'Override';
    const output = `${firstName} is ${age} years
                    old and is born in ${birthYear}`;
    console.log(output);
  };
  printAge();
  return age;
}
const firstName = 'Mahesh';
let age = calcAge(1999);

Now the firstName declared inside the printAge function is used, because the js engine searches for a variable in the parent scope, only if it's not in the current scope.

And the name 'Override' is kinda cool.

Override is 21 years old and is born in 1999

This explains why multiple functions can have the same argument names. Because the scope of the arguments end when the function finishes executing.

NOTE OF THE DAY: The window object has access to the var global variables. But it does not have access to the global const and let.

Closures : A complex application of scopes :-

So we would have come across some weird code blocks wherin functions return functions....WAIT WHATTT????? yes they doo....And vscode shows their prototype like this....

const doubleArrow: () => () => void

And they resemble this structure, they are also similar to the bind. But let's stick to the point.

The noob version of that funciton returning function would be

function greet(greeting) {
  return function (name) {
    console.log(`${greeting} ${name}`);
  };
}
greet('hey')('Mahesh');
// can also be written as
const inner = greet('Hi');
inner('Kevin Durant'); // same result

Let that sink in........!!!!!!

Now onto closures...

Inner functions have access to the parent function's scope, Even after they are returned. Yes, read that again...even after they are returned. To make it really clear...

A function always has access to the variable environment of the execution context in  which is was created.

Example time...

const secureBooking = () => {
  let passengers = 0;
  return () => {
    passengers  ;
    console.log(`${passengers} passengers.`);
  };
};
const booker = secureBooking();
booker();
booker();
booker();

Nothing happens even after the secureBooking function is called and is popped out of the execution context. Drama starts after the booker function is called...not one, not two, not three, not four, not five...alright cut it.. that was a james reference 😂. Coming back to cricket, the passengers variable was incremented 3 times.

// Output
1 passengers.
2 passengers.
3 passengers.

So even after the secureBooking functions comes out of the execution context , it is  able to access the parent scope variables (passengers).  The returned value which is a  function is stored in the booker variable. So the booker function now has access to the passengers variable which was  initially not in its scope.




Hoisting

The process of JS engine scanning thru our code and setting all the vars to undefined before they are assigned their respective values is called hoisting.

That was a long time ago.

Before es6, arrow functions, const , let etc.

Let us cover the easy part first.

  1. Hoisting sets all the vars to undefined at the start of execution, then when the code has started to execute line by line, these values change.
  • when u try to use a var before it is initialized, it returns undefined.
  1. The main advantage of hoisting is that functions can be called before they are declared. This was the sole purpose of hoisting. This was the main intention of hoisting.
  • This makes the code much much cleaner, (general opinion).

However, this causes numerous bugs in the code, when devs try to access the variables before they are initialized. These bugs are hard to locate. That's where const and let come into play.

  • const and let are identified by the compiler, but not hoisted. Therefore, when the program tries to access it before the initialization, JS yells at you saying 'can't access a variable before initialization'. However, if you try to access a variable which is never declared in your whole code, then JS yells differently saying 'can't access an undeclared variable'.

  • What happens in the case of function expressions and arrow functions?? DUDE!!!! they are nothing but variables acoording to our guy V8. Obviously they are treated the same way as const or let (in the weird case, if u declared an arrow func. as let).

  • Or if u declare a function expression as a var ..which is highly discouraged, then it is set to undefined during hoisting.

This explains why arrow functions can't be accessed before they are declared.

Nerd point :

const and let have something called a TDZ - (Temporal dead zone). Inside their scope, where they are identified by the compiler, until the declaration occurs, they are inside the TDZ. Whenever they are tried to used inside the TDZ , the 'uninitialized' error is thrown. TDZ is the reason why we are able to identify these errors.


The 'this' Keyword


It is a special variable that is created for the execution context of each and every function. this is not static, its value keeps changing everytime a different execution context is in top of the call stack. And the value is actually assigned to this when the function is actually called.

The value of this is highly affected by the type of the function inside which it exists...

  • Method functions: the this keyword points to the object which calls the method.

  • Simple functions: the this keyword is set to undefined. May also point to the window object of the browser depending on the mode.

  • Arrow functions: Usually don't have a this keyword and returns an empty object. But when surrounded by another function, the this keyword of the inner arrow function points to the this of the outer function.Therefore, if the outer function is a method, then this points to that corresponding object.

  • Event listeners: In this case, the this keyword points to the DOM element that is responsible for the event listener.

Examples....

  1. Method :
const person = {
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: function () {
    const age = 2020 - this.birthYear;
    console.log(this);
    this.age = age;
  },
  age: '',
};
person.calcAge();
console.log(person);

and the output for the above seen code block would be.......

{
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: [Function: calcAge],
  age: ''
}
{
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: [Function: calcAge],
  age: 21
}

If u look at the fist time the object is printed, the age object is still age:''. This comes from console logging the this object from inside the calcAge function. At that time, the this keyword points to the object that calls the function, which is the person object.

The second time the object is printed..... that is from the last line of the code wherein the person object gets updated after calling the calcAge function.

  1. Simple function :
const simpleFunc = function () {
  console.log('I am a simple function');
  console.log(this);
};
simpleFunc();

this above example is just a simple function definition and the subsequent call of the same. The this keyword inside this function returns undefined.

'I am a simple function'
undefined
  1. Arrow function Case :

Now this is a bit complicated, or atleast that's how I see it. First u have to understand that the example 1 for method function case can also be written in this form.....

const calcAge = function () {
  const age = 2020 - this.birthYear;
  console.log(this);
  this.age = age;
};

const person = {
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: calcAge,
  age: '',
};
person.calcAge();
console.log(person);

And the output would remain the same. Now let's start the complication process xD. Let me insert a useless arrow function inside the calcAge function expression.

const calcAge = function () {
  const age = 2020 - this.birthYear;
  // console.log(this);
  const printAge = () => {
    const firstName = this.name;
    const output = `${firstName} is ${age} years old
                     and is born in ${this.birthYear}`;
    console.log(output);
  };
  printAge();
  this.age = age;
};

const person = {
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: calcAge,
  age: '',
};
person.calcAge();
console.log(person);

Now all we are going to talk about is the printAge arrow function inside the calcAge function. Cus we are already pretty clear with the other parts of the code. Now the above written code gives the following output.

'Mahesh is 21 years old and is born in 1999'
{
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: [Function: calcAge],
  age: 21
}

Everything works perfectly. This is because...

this.name inside the arrow func. returns the name attribute of the this object which is currently pointing to the object that is called the outer function. And it's the same case with this.birthYear. But age is simply the variable in the outer scope.

This is what happens when an arrow function is inside a method and tries to access the this object. This was a new and insert fire emoji feature that was introduced in ES6.

BUT, (there's always a but)....What happens when we try to use arrow function as a object method and try to use this inside it directly.....

const calcAge = function () {
  const age = 2020 - this.birthYear;
  // console.log(this);
  this.age = age;
};

const printAge = () => {
  const firstName = this.name;
  const output = `${firstName} is ${this.age} years old and is
                    born in ${this.birthYear}`;
  console.log(output);
};

const person = {
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: calcAge,
  printAge: printAge,
  age: '',
};

person.calcAge();
console.log(person);
person.printAge();

I have taken the printAge arrow guy out of the calcAge method and declared it as a seperate method. Well, even after calculating the age and calling the printAge function via the person object...this is what turns out........

{
  name: 'Mahesh',
  birthYear: 1999,
  calcAge: [Function: calcAge],
  printAge: [Function: printAge],
  age: 21
}
undefined is undefined years old and is born in undefined

Enough said !!!!!

  1. Event Listeners :

This is pretty straight forward.

The this keyword inside the event listener points to the DOM element attached with the event listener.

// the event.js file
const header = document.querySelector('h1');
header.addEventListener('click', function () {
  console.log(this);
});
 <!-- The html file -->
  <body>
    <h1>Try clicking the header</h1>
    <script src="event.js"></script>
  </body>
// Browser console after clicking the header mulitple times

<h1>Try clicking the header</h1>                      event.js:3
<h1>Try clicking the header</h1>                      event.js:3
<h1>Try clicking the header</h1>                      event.js:3

Quick blooper : After finishing the first 3 parts, I went on to code the 4th part. Since I am an arrow lover, (not the Arrowverse)... i tried using an arrow function as the callback for the event listener in event.js and that eventually logged the window object to the console. YIKES !!!!!

Take-away : When in a browser, the this object returns the window object of the browser. But I find it comfortable to run plain js in my system with node.

node event.js

The this keyword inside the arrow function now returns an empty object {}.



OOPsssss!!!!!!!

Lemme get to the point straight away here, no messing around!

There are three ways to create an object.

  1. Function constructor

  2. ES6 classes which are just a syntactic sugar.

  3. Object.create()

Function constructor :

Okay, I am running out of time here, since my semester exams are fast approaching and the fact that me learning js is highly irrelevant to my exams makes me sick. Alright!! no messing around.

The general convention is that function constructors should start with a capital letters and they can only be a normal function and not an arrow guy. We'll get to know why.

const Person = function (name, age) {
  console.log(this);
};
new Person('Lebron', 36);

The output in the console would be an empty object, but like this Person {}

When a function constructor is called, it does four things.

  1. A new {} is created

  2. The function is called and the this keyword is set to that empty {}.

  3. {} is linked to something called a prototype.

  4. The function automatically returns the {}.

Now u guys would have prolly guessed the next step...

const Person = function (name, age) {
  this.name = name;
  this.age = age;
};
const lebron = new Person('Lebron', 36);
console.log(lebron);

The ouput would be exactly what we would have expected, an OBJ for LBJ.

Person {name: "Lebron", age: 36}

All we needed to do was to fill the empty this object.

Side note: Credits to vscode for yelling at me for using a 90s kids' method

const Person: typeof Person
This constructor function may be converted to a class declaration.ts(80002)

Technically speaking, in js when objects of a class are created, they are referred to as instances. But we did not create a class here. We can further improvise this ..

const Person = function (name, age) {
  this.name = name;
  this.age = age;
  this.calcByear = () => {
    const theWorstYear = 2020;
    this.birthYear = theWorstYear - this.age;
  };
};
const lebron = new Person('Lebron', 36);
lebron.calcByear();
console.log(lebron);

OUTPUT....

Person {name: "Lebron", age: 36, birthYear: 1984, calcByear: ƒ}

The reason i used calcByear as an arrow function because they don't have a this of their own and they inherit this.

A BIG BUT: using methods inside of function constructors is considered a very bad practice, because ,everytime an object is created using the constructor, a copy of that method is created. 1000 objects, okay..but 1000 copies of the same method??? nahhh!!

There is a workaround.!

const Person = function (name, age) {
  this.name = name;
  this.age = age;
};
const lebron = new Person('Lebron', 36);
Person.prototype.calcByear = function () {
  const theWorstYear = 2020;
  this.birthYear = theWorstYear - this.age;
};
console.log(Person.prototype);
lebron.calcByear();
console.log(lebron);

The output seems to be like this, make sure to take a look at the prototype of Person.

{calcByear: ƒ, constructor: ƒ}
calcByear: ƒ ()
constructor: ƒ (name, age)
__proto__: Object

The prototype has two functions now, the one that we created and the one that it owns. Also note that I have changed the arrow function to normal function because we don't want an inherited this keyword that point to the window object xD.

Now multiple copies of the function are not created.

How is this possible :

Because each object has access to a property called prototype or more specifically __proto__. lebron.__proto__ gives the same result as Person.prototype.

The functions that we load into the function constructor's prototype are inherited by the object's __proto__ attribute and that's why js is js.

console.log(Person.prototype === lebron.__proto__)
// true da dei

Nerd things: We can also inherit properties from prototypes, but it won't show up in the object until we ask for it....confusing, hell yeah !!!

Person.prototype.team = 'Los Angeles Lakers';
console.log(Person.prototype);
lebron.calcByear();
console.log(lebron);
console.log(lebron.hasOwnProperty('name'), lebron.hasOwnProperty('team'));
console.log(lebron.team);

Nothing up fishy up there, wait for the console output...

//prototype
{team: "Los Angeles Lakers", calcByear: ƒ, constructor: ƒ}
//object
Person {name: "Lebron", age: 36, birthYear: 1984} // no team property
// own property 'name' and 'team'
true false
//asking for  it
'Los Angeles Lakers - Champs2020'

Where did the extra work in the string come from. Well maybe js knows it xD.

Conclusion: The prototype chain is similar to the scope chain. JS looks for the property or method in the same object at first, or else, it looks in the parent prototype, in this case, the prototype of Person.

Final BOMB...

console.log(lebron.hasOwnProperty('hasOwnProperty')); //false

This searched first in the object itself, to be more specific, inside the {}. When it can't find this method, it jumps to Person.prototype. When it can't find it there too...It jumps further to the __proto__ property, which itself is an object, and there lies our guy -->, hasOwnProperty. Live with it !!!


Don't try this at home..

const header = document.querySelector('h1');
console.log(header.__proto__.__proto__.__proto__.__proto__.__proto__)
// and on and on and on

Nerd point

Object.freeze() is used to make an object immutable. One cannot add a new property, or erase an existing one, or rewrite an existing property after calling this method over that specific object. (Arrays are also objects - in case you forgot).

Example

const earnings = Object.freeze({
  joseph: 100000,
  vijay : 200000
})
earnings.thalapathy = 300000; 
// this above line changes nothing, the object is unaltered
// in strict mode, this throws an error
console.log(earnings);


ES6 classes

In es6 , classes are still implemented in the same way using prototypal inheritance. They are just a syntactic sugar.The only diff is that the syntax makes more sense. Let us jump straight into the code block.....

class Player {
  constructor(name, team, age) {
    this.name = name;
    this.team = team;
    this.age = age;
  }
  calBirthYear() {
    const theWorstYear = 2020;
    this.birthYear = theWorstYear - this.age;
  }
}
const kingJames = new Player('Lebron James', 'Lakers', 36);
const slimReaper = new Player('Kevin Durant', 'Nets', 32);
kingJames.calBirthYear();
console.log(kingJames);
slimReaper.calBirthYear();
console.log(slimReaper);
console.log(kingJames.__proto__ === Player.prototype);
console.log(slimReaper.hasOwnProperty('calBirthYear'));

output...

Player {name: "Lebron James", team: "Lakers", age: 36, birthYear: 1984}
Player {name: "Kevin Durant", team: "Nets", age: 32, birthYear: 1988}
true
false

The methods are definedjust like the constructor method, except that the constructor method need not be called, it is the one that initializes the object. The other methods that are defined in the class definition are not part of the object, but a part of their prototypes, as clearly shown in the last line. Just get on with the syntax now !

Note : Classes are not hoisted like function declarations or variables. And another point is that they can be returned from a function since they are 1st class citizens.


Async Code and the EVENT LOOP :-


Unlike JAVA, JavaScript is not multi-threaded.... but how does it acheive a Non blocking Concurrency model ?? Async functions like fetch, objects like Promise are executed not in the javaScript engine directly. They are executed with the help of web APIs. These async codes, once they are called, gets pushed to the call stack, but since they are asynchronous, they run in the background and the main thread isn't stopped. This is a pretty advanced concept and cannot be understood just by reading a paragraph written by some random Lebron James fan from South India. This needs a code block 😄.

console.log('test start');
setTimeout(() => console.log('timeout callback'), 0);
console.log('next line after setTimeout');
Promise.resolve('Promise resolved').then(res => console.log(res));
console.log('test over');

The output for this codeblock would be a little bit confusing....

test start
next line after setTimeout
test over
Promise resolved
timeout callback

In general, Promises are given priority in the execution stack over normal callback functions which are also considered asynchronous since they are destined to be called in the future. That is why, the 'Promise resolved' string was printed first before the 'timeout callback'.

But..wait what?? the delay for setTimeout was 0. ZERO !!

These files are not directly executed in the execution stack, therfore as soon as they are read by the JS engine, they are pushed to the background. Meanwhile, the global execution context is carrying on its work. After the global context is over, these results are printed by the JS runtime.

Note : If you have difficulty in understanding these concepts, (like i had at the time of scribling down this), please go back to the early parts of this doc, (just like i did).

About

An in-depth explanation of the working of JavaScript behind the curtains

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published