JavaScript 🌈

Let's learn a single threaded, multiparadigm and just-in-time compiled language

1. Language Basics (syntax and stuff)

Type conversion and type coercion

Type Conversion - Manually converting from one type to another

String to Number conversion

We can only convert strings that look like numbers to a number otherwise we get Not a Number

let year = '1991'
let month = 'jan'
console.log(Number(year)) // 1991 -> Number
console.log(Number(month)) // NaN -> Not a number

Click here for -> More On NaN

Type Coercion - When JS converts the type behind the scene

Type conversion happens automatically when the operator is working with two types of values and require to convert one of the values to match the other value type

console.log("I am " + 23 + "years old") // here 23 which is a number gets converted
//to string automatically by JS. This is type coercion

// If JS didn't have type coercion then we have to do this
console.log("I am " + String(23) + "years old") // but luckily we have type coercion
// Another example of type coercion - But string to number 
// we saw by using + we convert number to str

// Lets see this
console.log('23'-10) // gives 13 which is number
 
 // same here 
 console.log('2' * '3' * 2) // 12 number

Using + JS type coercion happens from Number to String

Using - or any other operator except + JS type coercion happens from String to Number

Example

Truthy and falsy values

Any value that gets converted to either true or false can be categorized as truthy and falsy values

Apart from the below-mentioned ones, all other values get true.

Falsy values are

  • 0

  • ' '

  • undefined

  • null

  • NaN

== vs ===

=== is a strict equality operator because it doesn't perform type coercion.

It returns true if both values are exactly the same.

== is loose equality operator and does the type coercion.

'18' == 18  // returns true
first '18' will be converted to a number by JS (type coercion) and then checks the 
equality

'18' === 18 // returns false
no type coercsion is done so str is compared to number so it gives false

Some of the confusing examples. Remember to type convert in your head and think what it should be if the given example is ==

when given lhs == rhs, to tell the output,

First convert lhs to match the rhs type or vice versa (which ever makes sense) and then apply === in your head to find the answer.

Example

'true' == true // false - this might be confusing

'true' is a string and true is bool.

string can't be converted to bool and vice versa so it will be false

Statements vs Expressions

Any code that gives values is an expression and any code that doesn't give a value but performs some action is called a statement.

// Expression - everything below gives or returns a value
123
const s = 'sa'
let b = 145


// Statement - doesnt give a value but does some action
if(true){
    break;
}

JS expects expressions in particular places so you should know that.

// Example in template literal, it expects expressions and not statments

console.log(`My age is ${2021-1992}`) // ${2021-1992} gives a value, so it's expression

// we can't have statements in template literals
console.log(`My age is ${if(true){}}`) // this is invalid

Conditional (Ternary) operators

// It's used as an alternative to if/else statement 

age >=18 console.log("drink wine") : console.log("drink water")

// Note that we can write one single line before and after : unlike if else

// we can use this when we have a one liner if else statement like this

age >= 18 "water" : "wine"

// now depeending on the age, it either returns water or wine. Since it returns
// something, the ternary operator is an expression

Null Vs Undefined

Undefined is set generally by JS. For example, for a variable, it sets undefined when it's just declared but not initialized. Even in case of hoisting. undefined means, something exists but not defined yet or yet to be defined.

let b;
console.log(b) // undefined

Null is generally set by the programmer and not JS. Null means nothing exists unlike undefined. Again, undefined means, something exists which is not defined yet, and null means nothing exists.

console.log(undefined+3) // NaN. Because undefined means something exists and not defined to which you cant add a number
console.log(null+3) // 3. Because null means nothing exists. It's as if writing just 3 because null is nothing or 0.

Scoping

Refer to the hand-written notes for scoping but here is the confusing part you need to be aware of about scoping.

Not defined vs not initialized

arguments

Javascript function defined with function keyword will have access to a special object called arguments. This is not present in arrow-function. This is Array-like structure which has access to the length variable and can be converted to array using Array.from() or spread operator -> [...arguments]. It's not an array so we can't access methods like map and forEach on this. We can do arguments[0], arguments[1]

// function with function keyword

function argTest(x, y) {
  console.log(arguments[0]) //2
  console.log(arguments[1]) //3
  const arg = [...arguments] // same as Array.from(arguments)
  console.log(arg) //[2,3]
}

argTest(2, 3)


// arrow function

const argTestArrow = (x, y) => {
  console.log(arguments) // ReferenceError: arguments is not defined
}

argTestArrow()

Deep copy

How can we deep copy?

function deepCopy(src) {
  let target = Array.isArray(src) ? [] : {};
  for (let prop in src) {
    let value = src[prop];
    if(value && typeof value === 'object') {
      target[prop] = deepCopy(value);
  } else {
      target[prop] = value;
  }
 }
    return target;
}

2. Modern Javascript development

Modules

  • Earlier we used to write all JS code into one big or multiple scripts and ship it to the browser

  • Today we write our code in modules and these modules can share data between them. We also include 3rd party modules and we call them packages as well

  • These modules are generally shared in the npm repository and then we can use them by downloading them using npm command-line-tool. Example react library, leaflet, and so on.

  • npm means Node Package Manager

  • Let's say we are done writing our code and done with the project. Now we divide that code into multiple modules and also include the 3rd party modules which were used in our project.

  • Now after building the modules, our project goes through a build process where one big final JS file is built. That is the final file that will be deployed to the web-sever for production

  • In the build process, first, we bundle all the modules into one big file. This is a complex process that will compress the code and eliminate unused code

  • we still combine into one big file because old browsers won't support modules and the module could not be executed by old browsers.

  • The second step is transpiling and polyfiling. This will convert all ES6 syntax into old syntax where the browser can understand. The transpiling/ polyfiling is done by a tool called Babel

  • After these two steps, we will finally have our JS file ready to be deployed to the server

  • We don't perform these steps manually, instead, we use build tools like Webpak or parcel. These are called Javascript bundlers as they bundle the code/modules into one single file

  • Webpack is the popular one but its a complex one as a lot of steps must be done whereas, the parcel has zero configuration to be done

Modules

// HTML file
// look at the type is module as we are using import and export functionality
    <script type="module" defer src="script.js"></script> // script
    <title>Modern JavaScript Development: Modules and Tooling</title>
    <style>

Review Udemy 268. Exporting and Importing in ES6 Modules for the above info

Module pattern

Module pattern was used before ES6 modules. The idea was similar to modules and we used to use functions to do the same and returned values (especially objects) were exposed or returned as a public API to be used in other JS script files

Common JS pattern

Besides the module pattern, there are other patterns. They are not native JS patterns like modules. They relied on some external implementations. Two examples are AMD modules and CommonJS modules.

CommonJS is important because they have been used for node js for almost all modules. Very recently the ES modules started to be implemented in node js. Node JS is, running JS outside of a web browser. All the common js modules which are used in node can also be used in our implementations. That's because npm was started only for node initially and has now become a module repository for the entire javascript world.

Just like ES6 modules, one file is one module in common js as well. We need to know how we export and import in common js

Exporting in a common js pattern. Note this doesn't work in a browser but it works in node js

Importing in a common js module.

NPM

Node Package Manager is a repository of JS modules and also a command-line tool to manage (install and update) these packages.

Earlier, we used to download different script tags for different things in JS. We used to download many JQuery plugins from different websites with different versions inside HTML files and it was all messy.

Drawbacks of the traditional way of downloading scripts

  • We don't get every script tag in one place

  • If they update the version we can't know that and we had to install it manually or change the script tag in HTML

How to use npm?

// Check the npm version
npm -v

// To use the npm on our project we need package.json file so we need to initialize npm
npm init

// package.json stores the entire configuration of our project

// Install a package/module which we earlier did with script tag
npm install leaflet

//Also can be done like this
npm i leaflet

//this creates a dependency for this in package.json

// To get back node module folder (all the packages installed in package.json)
npm install

// can also be done with
npm i
Now that we installed leaflet but it wouldn't be easy to use it without a 
module bundler like parcler or webpack because it uses a common js module system 
but we need ES6 module system so we can't directly import it into our code

The point is, we can't use common js modules without using a module bundler. Few packages have Es6 version as well as common js version. We can install like this

npm i lodash-es //not common js moudle but es6

this does not require a module bundler

Note: Lodash is a popular package/module containing useful functions for arrays, objects, dates, and so on

Example of using loadsh-es6

Let's use lodash to deepClone an object.

Without lodash it will be really difficult to implement it on our own in JS. We will later talk about how to implement this ourself but now let's use lodash to do this task

// Shallow Copy - Not yet using lodash for deep clone

const details = {
    name: 'sandeep',
    email: 'sa@g.com',
    family: {
        'dad': 'Gupta',
        'mom': 'Amrutha'
    }
}
console.log(details.family)

// Shallow copy an object
const updatedDetails = Object.assign({}, details)
updatedDetails.family.dad = 'Amaranatha Gupta' //changes original
updatedDetails.name = 'Deepu'// changing the name in the copy which doesnt affect
console.log(details.name) // sandeep - not changed
console.log(details.family) //{dad: "Amaranatha Gupta", mom: "Amrutha"}

// Line 18 changed because of line 15

Deep copy is difficult so let's use lodash to do that

// Using lodash for deep copy

import cloneDeep from './node_modules/lodash-es/cloneDeep.js'
// importing like this is not practical. 
//Let's use parcel (module bundler) to fix it later

const details = {
    name: 'sandeep',
    email: 'sa@g.com',
    family: {
        'dad': 'Gupta',
        'mom': 'Amrutha'
    }
}
console.log(details.family) // same as line 17

const updatedDetails = cloneDeep(details)
updatedDetails.dad = 'Amaranatha Gupta'
console.log(details.family) // same as line 13

Parcel - Module bundler

It's a build tool available on npm.

Note we have two type of dependency we get from npm

  1. Normal dependency - npm i module

  2. Dev dependency - npm i module --save-dev

Dev dependency is a tool or tools used to make a developer job easy and fast but our code doesn't need that. We don't include that dependency in our code

npm i parcel --save-dev

npm in parcel@desiredVersion --save-dev for a specific version

This was locally installed now. Only installed on to the folder you were in. To run this we have two options

  • Using npx command

  • npm script

1st way : npx - Is an app built into npm to run the locally installed packages

npx parcel index.html

For this to work, remove type=module in your html script tag

index.html is where the script lies which we need to bundle. What all gets bundled?

Well, the script itself and the imports used in the scripts all get bundled and compressed giving us a dist (distribution) folder that will deploy into production. This will have bundled files. It will also give us a new development server that gets updated on a refresh.

with parcel, we can configure hot module replacement

// script.js

if(module.hot){
module.hot.accept()
}
// only parcel understands this code

// This is used to reload only part of the changed content on the 
// page and not the entire page


//Also, on saving something, the existing info will get stacked 
//up on to new one. 
//Let's say my array has 4 values. I perform push again and save. 
//Now it becomes 5 values. Again if I save it will become 10 values and so on. 

Also, imports are now easy with parcel

import cloneDeep from 'loadsh-es'

we can now use common js modules as well because of parcel

import cloneDeep from 'loadsh'

2nd way to run locally installed packages - npm scripts

In package.json file, under scripts object, create a script for the one you need to run in the command line. For example

"scripts":{

"start" : "parcel index.html" // note we dont use npx here

}

Now to use/run this in the command line, use

npm run start

Once we are done with the development we need to build it

"scripts":{

"start" : "parcel index.html", // note we dont use npx here

"build" : "parcel build index.html"

}

After running this build, the dist folder files get compressed which can then be shipped to the browser

Babel and Polyfilling

Now that we activated bundling using parcel, it's now time to transpile (convert back) our modern es6 code to es5 code (browser understandable code)

Babel works with plugins and presets that can both be configured.

The Plugin is a specific JS we need to transpile. For example, the arrow function is a plugin we can transpile and leave the rest but that doesn't make much sense.

So instead of using plugins for transpiling each one separately, babel used Presets which is a bunch of plugins bundled together.

Transpiling

Converting the syntax of ES6 to ES5 in called transpiling. For example, converting arrow functions to regular functions, converting [...] into Array.concat and so on.

But what about new ES6 features like Promises and stuff. They don't have an ES5 equivalent syntax.

Polyfilling

Well that's when the polyfilling comes into rescue. The Promises and other modern features are converted not only syntactically but in a browser understandable way is called polyfilling.

Babel used to do polyfilling out of the box earlier but now they recommend other library for polyfilling. That is core-js package.

import 'core-js/stable' // for polyfilling every ES6 feature into ES5 but not async

For polyfilling async functions we need another package called regenerator-runtime

import 'regenerator-runtime/runtime' // used for polyfilling async functions

Best practices, Functional (Declarative) and Imperative Programming

Immutability is the best practice where we don't mutate the original array or object but instead copy them into a new one and then work with that. But how now to modify an object?

Using Object.freeze()We can't modify its values.

Limitation of Object.freeze()

Vite

Why did we need a bundler such as Parcel or Webpack?

Browsers didn't support modules so we had to convert our code written in modules to browser understandable code using a bundler.

This might slow down the performance and load time. How?

Let's say you have a thousand modules and the bundler will bundle everything before shown on the browser. If you try to edit a line of code and save it again, the bundler needs to re-bundle everything which is time-consuming. What if there was a way where, on saving it again after editing a line of code, only that part will be included in the pre-existing bundle. That's exactly what Vite does.

3. About JavaScript

History of JavaScript

Is JS pass by value or pass by reference?

Objects and arrays pass by reference, everything else, pass by value

4. Debouncing

Debouncing is a technique used in JS to reduce the number of function calls or API calls in order to improve performance (performance-optimization). Let's consider a few scenarios.

In a search box like the Amazon website, when you search for a product, notice that it won't suggest you the items present for every key you type. Suggestions appear only when you pause or stop typing. This is important because you avoid making unnecessary calls to API to fetch products matching the current keyword search.

Scroll

Consider a scroll behavior where you console log something at every point you scroll. This is not efficient. You might want to log only when you stop or pause scrolling.

This type of behavior can be achieved through debouncing.

To get to know about Debouncing, we need to know about setTimeout and clearTimeout. SetTimeout takes two parameters, a callback function and a timer in milliseconds. The callback gets executed when the timer expires. Also, the setTimeOut returns a number generally called timerID. This can be used to clear the timer and its callback function later before/after the timer has expired using clearTimeOut(). clearTimeOut(timerID) accepts timerID which was returned by setTimeout. Note that, the same timerID must be passed to clearTimeOut in order to clear out that particular setTimeOut().

const timerID = setTimeout(function () {
  console.log("Timer elapsed")
}, 2000)

// commenting below line let the setTimeOut to be executed and log "Timer elapsed"
clearTimeout(timerID)

How do we achieve debouncing with this? Our motive is to log, only when we stop pressing the button for 2000 milliseconds (2 sec).

const button = document.getElementById('btn')

function callWhenPaused() {
  console.log("I'm called")
}

function btnClck() {
  setTimeout(callWhenPaused, 2000)
}

button.addEventListener('click', btnClck)

The problem with the above code is that, when the button is clicked for the first time, it waits for 2 seconds before it prints. But the subsequent calls wouldn't wait so then it's called continuously as we press.

We can achieve this when we clear the previous timeout every time and call new setTimeOut. The new setTimeOut will execute only when we stop, meaning it will execute only when we don't clear it anymore when stop clicking it as shown below.

const button = document.getElementById('btn')

function callWhenPaused() {
  console.log("I'm called")
}

let timerID
function btnClck() {
  clearTimeout(timerID)
  timerID = setTimeout(callWhenPaused, 2000)
}

button.addEventListener('click', btnClck) 
// Note that the button is not the <button> but it's <input type="button">
// If it were <button> then, on click of it, it reloads the page so in that case we need to use the below


// If it were <button> instead of <input type="button" value="Click Me"/>
button.addEventListener('click', function (e) {
    e.preventDefault()
    btnClck()
})

This is actually debouncing. But there is a problem here. We have defined, timerID as a global variable. Generally, we need a debounce function as a higher-order function that we can use for different purposes like scroll or button clicks and so on. Closure makes it possible.

const button = document.getElementById('btn')

function callWhenPaused() {
  console.log("I'm called")
}

function debounce() {
  let timerID
  return function () {
    // closure is formed where timerID in the outer scope
    clearTimeout(timerID)
    timerID = setTimeout(callWhenPaused, 2000)
  }
}

const btnClck = debounce()

button.addEventListener('click', btnClck)

How can we now generalize debounce HOC for scroll and click events?

const button = document.getElementById('btn')

function callWhenPausedClicking() {
  console.log("I'm called when stopped clicking")
}

function callWhenPausedScrolling() {
  console.log("I'm called when stopped scrolling")
}

// Higher Order Debounce Function
function debounce(fn, timer) {
  let timerID
  return function () {
    // closure is formed where timerID in the outer scope
    clearTimeout(timerID)
    timerID = setTimeout(fn, timer)
  }
}

const btnClck = debounce(callWhenPausedClicking, 2000)
const scroll = debounce(callWhenPausedScrolling, 1000)


button.addEventListener('click', btnClck)
window.addEventListener('scroll', scroll)

We have one problem though. In debounce function, the context (this) is different in return function from fn

const button = document.getElementById('btn')

function callWhenPausedClicking() {
  console.log("I'm called when stopped clicking")
  console.log(this) // produces window without binding in debounce, and on bind, it produces the same which is button (same context (this) in return function in debounce)
}

// Higher Order Debounce Function where context of return function is different from fn context
function debounce(fn, timer) {
  let timerID
  return function () { 
    // closure is formed where timerID in the outer scope
    console.log(this)
    clearTimeout(timerID)
    timerID = setTimeout(fn, timer)
  }
}
function callWhenPausedClicking() {
  console.log("I'm called when stopped clicking")
  console.log(this)
}

// Higher Order Debounce Function
function debounce(fn, timer) {
  let timerID
  return function () {
    // closure is formed where timerID in the outer scope
    console.log(this)
    clearTimeout(timerID)
    timerID = setTimeout(fn.bind(this), timer) // now the conte
  }
}

Now we have achieved debouncing and the full code is below

5. Throttling

Throttling is another technique similar to debouncing used in JS to reduce the number of function calls or API calls in order to improve performance (performance-optimization). Let's consider a few scenarios.

Shooting game

Let's suppose you are playing a shooting game and you got a machine gun. Machine gun needs some time to load the bullet and can't shoot again immediately after shooting once. It takes some time to shoot again even though you continuosly press the trigger or button. This delay from one shoot to next shoot is called throttling.

Google Map resize

Consider you're using a map and then you zoom in the window to see where you are and zoom out again. Zooming from point A to point B can make some hundreds of API calls to get the location at every intermediate point which is not necessary. So we can implement throttling here as well so that once the map is resized it takes some time to get the location and not immediately.

Debouncing vs Throttling

Debouncing - Executing / calling some function or API only when the action is paused

Throttling - Executing / calling some function or API only in certain intervals of time where there will be specified time gap between consecutive function calls

The implementation is similar to debouncing but with little implementatiion changes, so please first refer debouncing to understand how setTimeout works and why we call a function that returns another function.

Let's now implement throttling for a shooting game (button click).

Simple btn-click without throttling

function shoot() {
  console.log("Shooting")
}

const button = document.getElementById('btn')

button.addEventListener('click', shoot) // calls shoot() continuously upon clicking

Now we introduce a function that limits the shooting and this function is the core of throttling behavior

function shoot() {
  console.log("Shooting")
}

// this below code flag + betterShoot() is in global scope 
let flag = true
function betterShoot() {
  if (flag) {
    shoot()
    flag = false;
    setTimeout(() => flag = true, 700)
  }
}

const button = document.getElementById('btn')

button.addEventListener('click', betterShoot)

Notice that the flag and betterShoot() are in global scope which is not what we want. We need to wrap up in a function and still need to get this functionality working

function shoot() {
  console.log("Shooting")
}

function throttle() {
  let flag = true
  // returning betterShoot() function
  return function () {
    if (flag) {
      shoot()
      flag = false;
      setTimeout(() => flag = true, 700)
    }
  }
}

const betterShoot = throttle() // throttle is a higher order function because it returns other function

const button = document.getElementById('btn')

button.addEventListener('click', betterShoot)

Generalizing this function by passing the parameters so that we can use throttle hoc for some other implementations

function shoot() {
  console.log("Shooting")
}

function throttle(fn, timer) {
  let flag = true
  // returning betterShoot() function
  return function () {
    if (flag) {
      fn()
      flag = false;
      setTimeout(() => flag = true, timer)
    }
  }
}

const betterShoot = throttle(shoot, 700) // throttle is a higher order function because it returns other function

const button = document.getElementById('btn')

button.addEventListener('click', betterShoot)

Passing the context from throttle hoc to fn

function shoot() {
  console.log(`Context inside fn function is ${this}`)
  console.log("Shooting")
}

function throttle(fn, timer) {
  let flag = true
  // returning betterShoot() function
  return function () {
    console.log(`Context inside inner function is ${this}`)
    if (flag) {
      fn.apply([this]) // can also use - fn.call(this)
      flag = false;
      setTimeout(() => flag = true, timer)
    }
  }
}

const betterShoot = throttle(shoot, 700) // throttle is a higher order function because it returns other function

const button = document.getElementById('btn')

button.addEventListener('click', betterShoot)

6. Numbers, Dates, and so on

JS is a base 2 system. So 0.1+0.2 is 0.30000000000000004

Be careful

if(0.1+0.2 === 0.3) -> false

Convert String to Number

  1. 1st way -> Number('23')

  2. 2nd way -> +'23' -> Type coersion happens here

Not a number (NaN)

NaN -> Not a number

Number.isNaN(x) gives the result which is unpredictable. To predict that first put x into Number(x) and then you put that inside isNaN(x)

Check if something is a Number or not (better than isNan())

Use isFinite() to check if it's number or not (even strings that look like numbers can be tested

isFinite(23) -> true
isFinite('23') -> true

// To check only if it's a number or not stictly then 
typeof 23 === 'number' // true
typeof '23' === 'number' // false

Use isInteger() to check if the number is a whole num of not

Number.isInteger(23) -> true
Number.isInteger(23.0) -> true because it is 23 itself
Number.isInteger(23.2) -> false

3 Ways to check if number or not in JS

  1. isNaN

  2. isFinite

  3. typeof somenumber === 'number' -> True means number else false

Read a integer out of a string

Number.parseInt('23ex') -> 23
Number.parseInt('23.4ex') -> 23
Number.pareseInt('ex23') -> NaN

Read a float out of a string

Number.pareseFloat('23.4ex') -> 23.4
Number.pareseFloat('ex23.4') -> NaN
Number.pareseFloat('23') -> 23

Rounding integers

4 ways to round the numbers - round, floor,ceil,trunc

// trunc,round, floor,ceil
console.log("------------Math.round()--------------")
console.log("Math.Round(23.2)" + Math.round(23.2)) // 23
console.log("Math.Round(23.7)" + Math.round(23.7)) // 24 
console.log("Math.Round(-23.2)" + Math.round(-23.2))// -23
console.log("Math.Round(-23.7)" + Math.round(-23.7))// -24

console.log("------------Math.floor()--------------")
console.log("Math.floor(23.2) " + Math.floor(23.2)) // 23
console.log("Math.floor(23.7) " + Math.floor(23.7)) // 23
console.log("Math.floor(-23.2) " + Math.floor(-23.2))//-24
console.log("Math.floor(-23.7) " + Math.floor(-23.7))//-23

console.log("------------Math.trunc()--------------")
console.log("Math.ceil(23.2)" + Math.ceil(23.2)) // 24
console.log("Math.ceil(23.7)" + Math.ceil(23.7)) // 24
console.log("Math.ceil(-23.2)" + Math.ceil(-23.2)) // -23
console.log("Math.ceil(-23.7)" + Math.ceil(-23.7)) //-23

console.log("------------Math.floor()--------------")
console.log("Math.trunc(23.2)" + Math.trunc(23.2)) // 23
console.log("Math.trunc(23.7)" + Math.trunc(23.7)) // 23
console.log("Math.trunc(-23.2)" + Math.trunc(-23.2)) // -23
console.log("Math.trunc(-23.7)" + Math.trunc(-23.7)) //-23

All of these methods does type co-ersion. Meaning, you can pass string '23' 
and it converts into number

Rounding decimals

console.log(+2.7.toFixed(3)) // 2.700
console.log(+2.7.toFixed(0)) // 3
console.log(+2.76945.toFixed(3)) // 2.769
console.log(+2.76955.toFixed(3)) // 2.770
console.log(+2.76965.toFixed(3)) // 2.770

toFixed() rounds the decimal (above 5 will round to the next number) and returns a string. We can add + to convert to a number or by Number(...)

7. DOM

Once you are familiar with theoritical concepts, you can do these excersises mentioned below

Excercises - Do once you study theory starting from DOM Nodes

If you can do the Tabbed Components then you're good until DOM traversing concepts

Udemy 191. Building a Tabbed Component

Build Hover handler by referring to

Udemy 192. Passing Arguments to an event handler

Sticky navigation

Udemy 193. Implementing sticky navigation

Sticky Navigation in a better way using Intersection Observer API

Udemy 194: A Better Way: The Intersection Observer API

Performance optimization of images - Lazy loading

Udemy 196. Lazy loading of images

Implement Slider

Udemy 197. and 198. Building a slider component Part 1 and 2

Theory

Document Object Modal

  • DOM is an API between Browser and JS. JS can also work independently of DOM. (NODE JS)

  • DOM is a tree like structure formed by HTML elements. DOM is an Object Modal of HTML

  • In the DOM there are different types of nodes. Example, some nodes are html elements while others are just text while others are just comments

What is Node?

  • Every single thing in the DOM (visualise as HTML Page) is of type Node.

  • Like everything else in JS, DOM node is represented by an Object.

  • This Node object gets access to methods and props such as

    • textContent -> Property

    • childNodes -> Property

    • parentNode -> Property

    • cloneNode() -> Method

  • The Node is classified into different types

    • Element -> has props like innerHTML, classList and so on (see below image)

    • Text

    • Comment

    • Document

  • The Element can be <p>, <h1>, <img> <a> and so on.

  • The Text can be the text inisde Element

  • The Comment is HTML comment

  • The Document contains methods like querySelector, createlement and so on to help other Nodes like Element, Text and so on.

  • The Element is again classified as different HTML elements like HTMLButtonElement for button, HTMLDivElement for div and so on. This is necessary because each HTML Element has different attributes such as src which is present only on the img and nothing else. Also, href present only on anchor tag.

  • EventTarget is a special Node type which is a parent of both Node and Window. EventTarget is the one that provides addEventListener() and removeEventListener() methods on each Node. Note that we never create EventTarget object manually. This all happens behind the scenes.

  • Window object is the global object having lots of methods and properties, lot are unrelated to DOM.

  • Children (Lower level elements) can inherit methods and properties from higher ups because of which all of these are possible.

Selecting document elements

  • Document (html) can be selected by document.documentElement

  • body can be selected by document.body

  • head can be selected by document.head

We can attach event liseners on these 3 directly without using querySelector. But attaching event listener to body makes sense and not other two.

Selecting elements

// give single result

const sinlgeResult = document.querySelector('.class--name') // gets the first element that matches the class

// gives node list

const nodeList = document.querySelectorAll('.class--name') // all elements that matches class

// first that matches ID
document.getElementById('id');

// all elements having btn tag
const buttons = document.getElementsByTagName('button') // returns Live HTML Collection unlike node list
// this can be applied to any tag name

// This buttons gives live html collection unlike nodeList which is by qsAll.
// A live HTML collection is updated when DOM changes but querySelectorAll which returns node list
// doesn't do that


document.getElementsByClassName('cls') // also gives us live collection.

// Live collection means, when we delete an element on the dom
// caused by live collection, then the collection get's updated immediately
// and the deleted element no longer exists inside the collection

// whereas in nodelist, even though the element is deleted in the dom, 
// it doesnt update it in the nodelist

Add to / remove from DOM (2 ways)

  1. .insertAdjacentHTML() method -> We write the HTML string and then pass it as a second param. 1st param will be the position to insert this string.

  2. document.createElement() method -> We get an object back and can set properties to it

1. insertAdjacentHTML(param1,param2) -

// syntax
element.insertAdjacentHTML(position, text);

position (param1) will be one of the following

  • 'beforebegin': Before the element itself.

  • 'afterbegin': Just inside the element, before its first child.

  • 'beforeend': Just inside the element, after its last child.

  • 'afterend': After the element itself.

Example 1

// <div id="one">one</div>
var d1 = document.getElementById('one');
d1.insertAdjacentHTML('afterend', `<div id="two">two</div>`);

Example 2

2. document.createElement(htmlElement)

now we can set the properties on this message like classList and so on. But how do we add this message to the DOM and where to add it?

Different places where we can put html elements in the DOM

insertAdjacentElement()

document.createElement()

beforebegin

before

afterbegin

prepend

beforeend

append

afterend

after

To insert multiple copies of same element

Remove a DOM element

remove() only removes the original message and not the clone of the message.

Not sure how to remove the clone node at this point yet.

the old way of deleting an element from dom. This is done using dom traversal. First, we select the parent of the element to be deleted and then delete the element

Get styles of a DOM element

We can set the style like this-> message.style.color = 'red'. This will be set as inline style for this message div. We can then select the inline style by using same message.style.color shown above.

But this will select only inline style color. If color is set in the CSS file then that will not get selected. To select that we use getComputedStyle(message). This is an object that has all the CSS styles written in style sheets (If not written in the style sheet, then also we get it becasue browser will assign default styles to that element and we get that). To get the height, for example, we can do getComputedStyle(message).height

Working with CSS Custom properties

The styles/variables defined inside the root so that they can be used later are called custom properties of CSS. Changing these values will change all the CSS where these are used

To select the entire document in JavaScript we use document.doucmentElement

console.log(document.doucmentElement) // will give us entire html document

With this, we can select CSS root element's style and change like this

document.documentElement.style.setProperty('--color-primary', 'orange')

HTML Attributes - Get and Set Attributes

Anything like src, alt, class, id etc defined inside the HTML element (example <img> ) are attributes.

Even though designer is defined in HTML, it still gives undefined. If we define designer in HTML as an attribute and want to access it in JS then we can use logo.getAttribute('designer')

Classes add and remove

Data Attributes

We can set an attribute that starts with data-. This can be useful in defining prop

Implement Smooth scroll

Smooth scrolling means, scrolling automatically to a particular position itself, maybe on a button click or some other event.

getBoundingClientRect() method is used to get the properties of an element like the exact position of that element in the browser

Get scroll position of the page

window.pageXOffset and window.pageYOffset gives the current scroll position of the page. If not scrolled then it will be 0 and 0

Get the width and height of the viewport

After learning these, now you can understand how to Scroll to a particular section

Summary of getting element postion on page

How to get

Here's how to get

Get the entire document

document.documentElement

Get the document body

document.body (NOT document.documentElement.body)

Get the document head

document.head

Get pre-defined attributes

document.qs(img).src (This is an example)

Get user-defined attributes

document.qs(img).getAttribute('design') (pre-defined can also be got this way. document.qs(img).getAttribute('src') )

Get data attributes

document.qs(img).dataset.versionNumber

Get position (co-ordinates) of an element on the page

document.qs(img).getBoundingClientRect()

Get scroll position

window.pageXOffset, window.pageYOffset

Get height and width of the view-port

document.documentElement.clientHeight document.documentElement.clientWidth

Scroll to a particular section (OLD WAY)

window.scrollTo(section.getBoundingClientRect().left + window.pageXOffset, section.getBoundingClientRect().top + window.pageYOffset)

Smooth scroll (OLD WAY)

window.scrollTo({left : same left above, top: same top above, behavior : smooth})

Scroll to a particular section (MODERN WAY)

section.scrollIntoView()

Smooth scroll (MODERN WAY)

section.scrollIntoView({behavior:smooth})

Adding/Removing Event Listeners

Two ways to add an event listener.

  1. addEventListener()

  2. onEventName = function() -- examples: onmouseenter, onclick and so on.

2. onEventNameis the old school method and is not used much. The most used is addEventListener and the reason is, in theaddEL, we can attach multiple events one after the other and the second callback will be different from the first one so both of them get executed one after the other, whereas, in onEventName = function, we can't do that. The second one overwrites the first one.

Also, we can remove the eventListenerwhen we implement 1st method.

Removing Event Listener ( It can't be done in 2 - onmouseenter one)

Event-Propagation (Capturing and Bubbling)

3 phases exist for most of the events.

  1. Capturing phase

  2. Target phase

  3. Bubbling phase

When an event happens like 'click', the capture phase begins. From document to the child element the event propagates before reaching the target (child where the event happens like 'click').

The target phase happens when the event reaches the child at which click happens. Here the event can be handled, meaning the event listener code attached to that child gets executed.

After the event reaches and executed in the target phase, the bubbling phase begins. Bubbling means, that event will now pass from the target (child at which the event happened) to the top-level parent through all the parents not reaching the siblings of course.

As the event bubbles up, the event is handled by every parent also. This means that if we attach an event listener to the section element (which is one of the parents of the target child), then that event at the section also gets executed as shown in the image below.

We would handle the event at each and every parent in the bubbling phase.

By default, the event could only be handled in the target phase and the bubbling phase. But we can set up the event handlers to handle events in capturing phase instead. For this, we set the third param to trueinside addEventListener() which is false by default.

This entire process is called event propagation, as events are propagating from one place to another.

With the event bubbling, we could implement really powerful patterns

Not all types of events will have the capturing and bubbling phase. Some events are created at the target itself and handled there

Example of Event-bubbling

Target

Current Target

Stopping the event propagation

In practice, it might not be a good idea to stop the event propagation but it might be a good idea sometimes depending on the use case but it's really rare.

Event bubbling is very useful for event delegation (next topic) but capturing phase might not be that useful. But still, we can handle events in capturing phase by setting the third param to true

Event handling in capturing phase

Event Delegation

Event delegation is possible because of event bubbling. Instead of targeting the particular nav__link, we target the common parent (common to all nav__link elements) and then we can attach a single event listener to the common parent which is nav__links, and then use target (clicked nav__link) to select the one clicked and apply the functionality on to that.

Without event delegation

With event delegation

DOM traversing

Walking through the DOM is called DOM traversing. Meaning we can select an element based on another element. We have parents, children, and siblings on which we can select.

Going downwards - Selecting Children

1st way of selecting children is by using querySelector and querySelectorAll on the element (not on document). It will select children - deeper children also get selected

If there are other elements with highlight class, they wont be selected. Only the children of h1 having this class gets selected

To get the direct children we use .children

To get the first and the last child

Going upwards - Selecting Parents

To select the immediate parent

To select the parent which is far away (multiple levels higher)

Going sideways - Selecting Siblings

In Js we can only select direct siblings (previous and next sibling), but if we want all siblings then the trick is to point to direct parent and get all direct children except the child we are on.

Summary of dom traversing methods

Children Selectors

  • element.querySelector/All('.someclass') // Selects one or all children having someclass

  • element.children // Direct children

  • element.childNodes // Direct child nodes - not used much

  • element.firstElementChild // first child

  • element.lastElementChild // last child

Parent Selectors

  • element.parentElement // get direct/immediate parent

  • element.parentNode // get direct/immediate parent node - not used much

  • element.closest('.someclass') // get parent which is multiple levels up having someclass

  • element.closest('element') // get that element itself

Sibling Selectors

  • element.previousElementSibling // get the previous sibling element

  • element.nextElementSibling // get the previous sibling element

  • element.previousSibling // get the previous sibling node - not used much

  • element.nextSibling // get the next sibling node - not used much

  • element.parentElement.children // get all children and then if loop to unselect the element to get all siblings

Build tabbed component by referring to udemy 191. Building a Tabbed Component

Build Hover handler by referring to udemy 192. Passing Arguments to event handler

The biggest take away from Udemy 192 is -

  • We cannot pass multiple arguments into an event handler function. It can have only one argument which is the event by default. If we need to have a second param, then we. need to use the arrow function to call this event handler function.

  • The default argument that is passed (we don't need to pass as a parameter) into the event handler function is "event" argument. That is present by default

  • The only argument we can manually pass is 'this' keyword by using bind method

  • If we really want to pass multiple arguments then use the arrow function inside addEL. In this case, we need to manually pass event as shown in the image below

TODO Implementing Sticky Navigation

With window scroll

With Intersection Observer API

Note: To select the src (img) we do document.querySelector('#id').src and not

document.querySelector('#id').style.src

The scroll event is on window and not document.

window.addEventListener('scroll',function(){

console.log('Fired each time you scroll') // we don't need event

})

Lazy loading of images - Udemy 196. Lazy loading of images

Implement Slider - Udemy 197. and 198. Building a slider component Part 1 and 2

Webpage lifecycle DOM Events

3 different DOM events that occur in a webpage's lifecycle - right from the page is accessed until the page is closed by the user

1. DOMContentLoaded Event - (Fired on document object)

This event is loaded by the DOM as soon as the HTML is completely parsed. HMTL parsing means downloading the HTML and converting it to the DOM tree.

Also, all scripts must be downloaded and executed before this event happens.

This doesn't wait for images and other resources to load. Just HTML and JS need to be loaded.

"We want all our JS code to be executed only after the HTML is downloaded and available. Does that mean we need to wrap our entire JS code with this event?

Actually not required because our javascript is imported into HTML at the end of the HTML file inside script tag. By the time we import the script, we would have downloaded and parsed the HTML and have the DOMContentLoaded fired-of.

There are other ways of loading javascript with the script tag but more in the next section about it.

2. Load Event - (Fired on window object)

The load event is fired by the window as soon as not only after HTML is parsed but also after images, CSS and other resources are loaded. Meaning when the complete page is loaded this event is fired.

3. Before Unload Event - (Fired on window object)

Created immediately before the user is about to leave the page. Useful in the case when the user has forgotten to save his data. It gives a prompt (warning) if we try to leave.

Async vs Defer script loading (Async and Defer is HTML5 feature and not the JS feature)

The biggest point to remember is that the DOMContentLoaded will only fire-of after HTML is completely parsed. But, DCL might be loaded before JS is executed or after JS is executed depending on the situation.

I have a little confusion in DOMContentLoaded. If script is first exectued and DCL is loaded next then how is that possible to get our JS code working? Need to dig deeper.

TODO: Understanding DCL a bit clearly

Found the answer to the above question.

See you have to understand one thing. DCL event is executed once HTML is completely parsed. That doesn't mean that, JS can't be executed before DCL.

JS scripts can be executed before or after DCL (it doesn't matter). DOM gives us a method called DCL (Dom Content Loaded) just to tell us that "HTML is completely paresed, now do whatever you want". JS can be executed even before HTML is parsed (i.e., even before DCL is fired)

Useful article about async and defer

Note: To submit a form on enter button, you can do two ways:

  1. Use event listener on form and then listen for keydown and then if the key in the event is "Enter" then it means it's submit

  2. The more elegant way is, the form which is document.qs('form') will have an event called submit. Make use of that as shown below

Note: When select HTML element is changed, the 'change' event occurs on which we can listen to

Note: How to listen to URL changes? -> Using hashchange event

// Let's say On a anchor click, my hash changes 
 // FROM 
//https://forkify-v2.netlify.app/#5ed662fb2cb51e00175a342a
 // TO
//https://forkify-v2.netlify.app/#5ed662fb2cb51e00175a342a

// we can monitor this with 

window.addEL('hashchange',callbackfun)

// To get the hash:

window.location.hash // #5ed6604591c37cdc054bcc40

8. Local storage

Local storage stores key, value pairs. Both key and value must be a string. If you want to store an object as a value then convert the object to the string by doing JSON.stringify(myobject).

//setting data to localstorage

localStorage.setItem('name','sandeep')
const employee = {'name':'sandeep','email':'s@g.com'}

localStorage.setItem('employee',JSON.stringify(employee))

//getting data from localstorage
localStorage.getItem('employee')

//remove items
localStorage.removeItem('name')

Local storage is blocking. Meaning, it's synchronous. So we need to set only small data here. Otherwise, it slows down the browser.

Object to string ----> JSON.stringify(object)

String to Object -----> JSON.parses(string)

There is one problem converting an object to string from local storage. That is the prototype chain. The prototype chain will be lost and does not work. The problem occurs when converting an object to string back to an object.

How to reload a page?

location.reload().

9. Arrays

Useful array methods

Array Method

Description

Returns new / modify the same array

push(ele)

Add to the end

Modifies the same - returns length of arr

unshift(ele)

Adds to the beginning

Modifies the same - returns length of arr

pop()

Removes at the end

Modifies the same - returns removed element

shift()

Removes at the start

Modifies the same - returns removed element

indexOf(ele)

Gives the index of first element

we pass. -1 if element

not present

Doesn't modify

includes(ele)

Gives true if present else false

Doesn't modify

slice(begin)

Starts on begin index and gives until end

Gives new array. Doesn't modify original

slice(begin,end)

Starts on begin index and gives all the way until but not including end

Gives new array. Doesn't modify original

slice(-2)

Gives last two elements

Gives new array. Doesn't modify original

slice(0)

gives back all elements. Same as slice()

Gives new array. Doesn't modify original

splice(begin)

starts on begin index and gives until the end. Imagine taking original array elements and giving it in an array

Changes the original array

splice() is generally used for deleting elements and inserting elements of original array

splice(begin) works similar to slice(begin) except it changes the original array

splice(begin,deleteCount)

From begin, it deletes "deleteCount" number of elements

Changes the original array

splice(begin,deleteCount,insert)

From begin, it deletes "deleteCount" number of elements and inserts element or elements at insert. We can have multiple values inserted as shown in the next one below.

Changes the original array

splice(begin,deleteCount,insert1,insert2)

Changes the original array

splice(4,0,"a","b")

From position 4, it deletes 0 elements and inserts a and b. So a,b will be inserted at 4th and 5th index

Changes the original array

splice(0, 0, "x", "m")

inserts two elements at the beginning

Changes the original array

reverse()

reverses the original array

Changes the original array

arr.concat(arr2)

gives same as [...arr,...arr2]

Gives new array. Doesn't modify original

split(splitter)

splits the string on splitter

gives array. Doesn't modify original

join(joiner)

joins all array elements into a string by joiner

Gives new array. Doesn't modify original

[1,2,3,4].join('/')

1/2/3/4

[1,2,3,4].join('')

1234

[1,2,3,4].join()

1,2,3,4

same as join(',')

find(callback)

arr.find(el => el <0)

gives the first negative element. Same like filter but gives one single element unlike filter

findIndex(callback)

same as find but returns index instead of element

Let's say you want to delete an element using it's index and you define certain condition to identify that element, then this method is useful.

some(callback)

includes(something) is used to check if something which is an element exists. If we want to check if an element with certain conditions exist then we use some(). arr.some(ele=>ele>100)

Doesnt mutate original array. Instead returns a single element that matches the callback condition. THIS RETURNS true or false

every(callback)

Same as some() but only returns true if all of the elements match the callback condition

THIS RETURNS true or false

flat()

flattens the array one level deep. flat() is same as flat(1)

Gives new array. Doesn't modify original

flat(depthLevel)

Let's say we have array inside an array inside an. array (nested arrays) then we can pass a number -> flat(3) to unflatten the array

Gives new array. Doesn't modify original

flatMap(depthLevel)

When we chain methods, we end up using map().flat(). flatMap() combines these two. Please refer EXAMPLE BELOW.

sort()

sorts the array in alphabetical order. Works on strings. Even numbers are treated as strings.

Changes the original array

sort(callback)

sorts the array based on callback. If callback returns >0 then the elements gets sorted in ascending order. If a<b then array gets sorted in descending order. If returns 0 then it remains as is.

Changes the original array.

EXAMPLE BELOW

fill(element,start,endButNotIncluded)

Replaces elements from start to end but not including end with the element specified

Changes the original array.

Fill is the only method works on array created with new Array(length)

from({},())

Used to create arrays. EXAMPLE BELOW

Creates new array

Flat and FlatMap example

Sort example

// sort()

const arr = [9, 2, 4, 1, 10, 0]

console.log(arr.sort()) // [0, 1, 10, 2, 4, 9]

// sort(callback)

// for ascending 
console.log(arr.sort((a, b) => a - b)) // [0, 1, 2, 4, 9, 10] same as returning above 0

//for descending
console.log(arr.sort((a, b) => b - a)) // [10, 9, 4, 2, 1, 0] same as returnig below 0

//for as it is (not required to sort but just showing what happens if returns 0
console.log(arr.sort((a, b) => b - b)) //[9, 2, 4, 1, 10, 0]

// Note that a will be second element of array and b will be first element

Array destructuring

// getting array elements
const arr = [1,2,3]
// instead of arr[1], arr[2]
//we do

const [a,b,c] = arr;
// now a=1, b=2,c=3. need not be a,b and c. It can be anything


// swapping variables without using temp

let [main, , secondary] = [1, 2]
console.log(main, secondary);
[main, secondary] = [secondary, main];
console.log(main, secondary); // 2,1 // using array destructuring

// nested array destruct
const arr = [2, 3, [4, 5]]

const [a, , [c]] = arr
console.log(a, c) // 2,4

// defaults

const [p, q, r = 10] = [2, 3]
console.log(p, q, r) // 2,3,10

For Each loop vs for of loop

For each on maps

for each on set

Map, Filter, and Reduce

Reduce

// Map and filter are easy so just explaining reduce here

const arr = [9,45,65,78]

const reduced = arr.reduce((sum, currentVal,index,array) => {

        // first iteration
        
        // sum -> 500 (if initial val is not given then sum is 9 first time)
        // currentValue -> 9(if 500 is passed) or 45(if 500 is not passed) 
        // index -> 0 (if 500 is passed) or 1 if(500 is not passed)
        // array -> arr

        return 5 // this 5 becomes the sum for next iteration. 
                 // If nothing is returned then sum will be undefined

},500) // 500 is the initial value. If not given then arr[0] becomes initial value


// What are optional here
// --> initialvalue, index, array


// How many elements atleast my array should have for reduce to work?
// --> 1 element. If I have only 1 element then reduce is not reached. 1 is simply returned

// What if my array has 0 elemets?
// We get error - Type Error



// EXAMPLE OF reduce. To add all elements of array
arr.reduce((sum,cur) => sum + cur) // 9+45+65+78

find method

Just like the filter method but gives the single element, unlike the filter which gives the array.

const movements = [200, 450, -400, 3000, -650, -130, 70, 1300]

const found = movements.find((mov) => mov < 200)
console.log(found) // -400

fill method

This is the only method that fills the empty array which is created with new Array(length)

const array = [1, 2, 3, 4, 5]

console.log(array.fill(34, 1)) // from 1st index, all are replaced with 34
// gives [1,34,34,34,34,34]

console.log(array.fill(34,1,3)) // [1,34,34,4,5]


// Fill an empty array

const emptyArrayOflength4 = new Array(4) // [empty * 4)

emptyArrayOflength4.fill(10) // [10,10,10,10]

Ways to create Array

// 5 ways of creating arrays

const array = [1, 2, 3, 4, 5]
const arrayUsingConstructor = new Array(1, 2, 3, 4) // same as above 
const arraySingleConstructor = new Array(8) //creates 8 empty spaces

console.log(array) // [1, 2, 3, 4, 5]
console.log(arrayUsingConstructor) //  [1, 2, 3, 4]
console.log(arraySingleConstructor)// [empty × 8]

// The next way of creating an Array is using Array.from(). Let's see that next

Array.from()

// how to create an empty array of length 7 and fill it with 10

// 1st way

new Array(7).fill(10) // [10,10,10,10,10,10,10]

// 2nd way
Array.from({ length: 7 }, (() => 10)) // [10,10,10,10,10,10,10]

// Array.from() takes two params. 1st one is an object where we can 
// specify the length and next param is the function expression that can take
// a function and return a value. The returned value is filled in array.

// Advantage of Array.from() is that we can get access to index (2nd parameter)
// within the inner function so that we can create continuous array elements 
// as shown below. 

// Lets create [1,2,3,4] using Array.from()

  Array.from({ length: 4 }, ((_, i) => i + 1)) // [1,2,3,4]
  
  // The advantage of this is that we can even create 1000 elements very fast
  
  // The second usecase of Array.from() is to create array from iterables like
  // string, maps, sets
  

 // 1. Create array from another array
const mainArray = [1, 2, 3, 4]
const fromMain = Array.from(mainArray)
console.log(fromMain) // [1, 2, 3, 4] . This is same as [...mainArray]

// 2. Create array from map
const map = new Map([
  [1, 2], [3, 4], [4, 5]
])
console.log(Array.from(map)) //[[1, 2], [3, 4], [4, 5]]

//3. The other usecase is when we have querySelectorAll, it gives a nodeList 
// which is not an array but has array like structure. That can be converted
// to array using Array.from()

// The Use Case of qsAll: Let's say we have multiple values present on UI
// and we need to calculate the sum of it, then we can use the above metnioned point

// The next way of creating array is below

Another way of creating a larger array holding continuos elements

  // For numbers
  
// ids = [0, 1, ...., 2999]
  const ids = [...Array(3000).keys()];
  
  
  
  // For strings - If you want to convert each number to string then,
  
  // ids = ["0", "1", ...., "2999"]
  const ids = [...Array(3000).keys()].map((n) => n.toString());
  
  
  // NOTE :[...[4,3,5].keys()] will give you indeces of array elements. The result of 
  // [...[4,3,5].keys()] will be 0,1,2
  
  // [[4,3,2].keys()] will give you iterator which you can use inside for-of loop, 
  // so we just spread them (gives back array instead of iterator) to access inside map

Ways to check if it's an array or not

// Two ways to check if the given element is an array
console.log(Array.isArray([1, 2,])) // true

console.log(
  [1, 2, 3] instanceof Array
) // true

Summary Of Array Methods

A simple coding challenge I failed to solve which is straight forward.

const array = ["sandeep","vijay","naveen"]

// print -> sandeep and vijay and naveen are friends

I failed to do in simple way but used all unncessary methods. the simple way to go here with isjoin()method.

// So the solution is,

console.log(array.join(" and ") + " are friends")

10. Objects

Objects are Key-value pairs

Two ways to access the values - Dot notation and the Bracket notation

In bracket notation, we can compute values and then get it from object whereas in dot notation we can just get the values directly from object without computing.

We get undefined when we try to access the property that's not present in the object.

Useful Object methods

Object method

Description

Object destructuring

// order of destucturing doesnt matter but the key names should be exactly same

const obj = { 'a': 'A', 'b': 'B', 'c': 'C' }
const { c, a } = obj
console.log(c, a) // 'C', 'A'

// assigning different names
const obj = { 'a': 'A', 'b': 'B', 'c': 'C' }
const { c: cc, a: aa } = obj // note we don't use 'cc' and 'aa'. We dont use ''
console.log(cc, aa)

//setting defaults
const obj = { 'a': 'A', 'b': 'B', 'c': 'C' }
const { c: cc, a: aa, d = "D" } = obj //if d deosnt exist then this will be used
console.log(cc, aa, d)

// assigning different property names and assigning default values
const obj = { 'a': 'A', 'b': 'B', 'c': 'C' }
const { c: cc, a: aa, d: dd = "D" } = obj // see d
console.log(cc, aa, dd)

// Mutating the variables using ()

let x = 111;
let y = 222;
let obj = { 'x': 'X', 'y': 'Y' };
({ x, y } = obj);
console.log(x, y)


// Nested object destructuring

openingHours: {
  thu: {
    open: 12,
    close: 22,
  },
  fri: {
    open: 11,
    close: 23,
  },
  sat: {
    open: 0, // Open 24 hours
    close: 24,
  },
},

// this is what we need -> open of fri

// const { fri } = openingHours
// const { open } = fri
// console.log(open)

// above 3 lines can be written as

const { fri: { open, close } } = openingHours
console.log(close)

Method Borrowing

11. Modern Operators

Spread and Rest operators (Expand and Compress)

Spread operator - ON RIGHT-HAND SIDE OF =

Used to unpack the array, string and object elements

const arr = [4, 5, 6]
const newArr = [0, 1, 2, ...arr]
console.log(newArr) // [0,1,2,4,5,6]

// creating a shallow copy of arr
const newArr = [...arr]

// you can use this method to join two arrays

// we can use the spread operator only in an array or while passing params to function
console.log(...arr)

// shallow copying objects
let obj = { 'x': 'X', 'y': 'Y' };
const newObj = { 'found': 'Sandeep', ...obj }
console.log(newObj)

// the other way to shallow copy object is by using Object.assign(target,src)

Rest pattern/rest operator - ON LEFT-HAND SIDE OF =

Looks exactly like spread operator but works in an opposite way

Spread - Unpacks or spreads elements

Rest - Packs or collects the elements into array.

Rest is used with destructuring generally and must be the placed at last of destructuring

// observ, we use ... to the lhs of = in rest pattern to collect elements into array
// lhs is destructuring as we discussed
const [a, b, ...c] = [1, 2, 3, 4, 5, 6]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3,4,5,6]


// Using both rest and spread (rest -lhs and spread -rhs)
const arr = [1, 2, 3, 4, 5, 6]
const [a, b, ...c] = [...arr]
console.log(a) // 1
console.log(b) // 2
console.log(c) // [3,4,5,6]

// same works for objects. remaining elements will be collected into object


// The other usecase of rest is in function parameters.

function add(...numbers){
  // add all numbers
  // all numbers will be in collected in an array
}
add(1,2,3,4,5,6)

There can be many spread but one single rest

// CAN HAVE
const [a, b, ...c] = [...arr1,...arr2,...arr3] // multiple spread

// CANT HAVE
const [a,b,...c,d] = [...arr1,...arr2,...arr3]

// ASLO CANT HAVE
const [a,b,...c,...d] = [...arr1,...arr2,...arr3]

Short circuit operator (&& and ||)

||

// if first value is truthy value, it will immediately return that
console.log(3 || 'sandeep') // 3
console.log(0 || 'sandeep' || 3) // sandeep
console.log('' || 0 || {} || 4) // {}, because {} is truthy value

// if it cant find any truthy then it returns the last one even though it is falsy
console.log(0 || '') // '' 

&& (opposite to ||)

// evaluates all the values and as soon as it finds the first falsy val it returns
// if it finds all truthy values then it returns the last truthy value

console.log(0 && 'sa') //0
console.log(1 && 'sa') //sa

Nullish coalescing operator (??)

// Problem with || operator
restaurant.numGuests = 0
const res = restaurant.numGuests || 10; // gives 10
console.log(res) // gives 10 because guests are 0. But this is not we want. 
// We want it to tell the guests are 0
// the reason this happens because || considers 0 as falsy and returns 
// the next one which is 10 


// so when we genuinely expect 0, since || considers the 0 as falsy it moves
// to further evaluating the code

// We can solve this using nullish operator ??
// Solution 
restaurant.numGuests = 0
const res = restaurant.numGuests ?? 10; // gives 0
console.log(res) //0

Optional chaining (?.)

Used to check if a property on an object exists or not

console.log(restaurant.openingHours.mon) // undefined
console.log(restaurant.openingHours.mon.open) // error because we are doing undefined.open

// To solve this we can do
if(restaurant.openingHours.mon){
console.log(restaurant.openingHours.mon.open)
}

// The better way is to use optional chaining
console.log(restaurant.openingHours.mon?.open) //undefined

// this means, saying "if restaurant.openingHours.mon exists then only evaluate further
// if not return undefined

// you can also check if openingHours exists then evaluate further
console.log(restaurant.openingHours?.mon?.open) 


// we can also use to check if method exists
console.log(restaurant.order?.(0, 1) ?? 'Method doesnt exisit')

for-of loop

continue and break can be used unlike forEach loop

Works for Iterables - array, string, maps, sets

We can use it on objects as well in an indirect way

const arr = [1,2,4]

for(const element of arr){
 console.log(element)
}

//when we want index, then we should call .entires() method on arrays
for (const [index, element] of arr.entries()) {
  console.log(index, element)
}

// note, in .exntries we get index,elements

For objects, we can use Object.keys(object) This gives an array of all keys. To get values we can do Object.values(object). This gives values.

To loop over the keys and values of an object we can make use of Object.entries(object)

const properties = Object.keys(openeingHours) // gives an array. On this we can do for-of
for(const ele of properties){
 // code
}


// keys and values
  openingHours: {
    thu: {
      open: 12,
      close: 22,
    },
    fri: {
      open: 11,
      close: 23,
    },
    sat: {
      open: 0, // Open 24 hours
      close: 24,
    },
  },
};

for (const [key, value] of Object.entries(restaurant.openingHours)) {
  console.log(key, value['open'])
}

Note the difference of entries() method between arrays and objects

// For Array

const arr = [1,2,3]

for(const ele of arr.entries()){

}

// For Objects
for(const ele of Object.entries(openingHours)){

}

for-in loop (old method- use for -of now)

Works for objects - maps over keys of objects

const obj = {
  'a': 'A',
  'b': 'B',
  'c': 'C'
}

for (const element in obj) {
  console.log(`${element} - ${obj[element]}`)
}
// OUTPUT
a - A
b - B
c - C

12. Sets

  • Set is used to return an object containing unique elements

  • Only iterables can be passed into set. We can pass array, string, map, set but not object

const setString = new Set("aaeei")
console.log(setString) // {'a','e','i'}

const setArray = new Set([1, 2, 3, 3, 4, 4])
console.log(setArray) // {1,2,3,4}

// Properties on set
setString.size // 3 - length of the set

setString.has('e') // true   -- similar to includes in array

setString.add('x') // added x only once no matter how many times we add x

setString.delete('x') // returns true if deleted and false if not

setString.clear() // deletes all the values from set

// NOTE : we can't get values out of set like setString[0]. This is invalid
// We don't need to get the values so set is not designed to get the values

The main use-case of a set is to eliminate repeated values in an array and give back only the unique elements

const setArray = new Set([1, 2, 3, 3, 4, 4])
const uniqueArray = [...setArray]
console.log(uniqueArray)

The other use-case might be to check how many unique letters are there in a string

const str = 'bookkeeper'
console.log(new Set(str).size) //6 - b o k e p r

13. Maps

Key-value pair like object but one of the differences is, the map can have keys which are functions, strings, maps and so on.


const map = new Map();
map.set('name', 'sandeep') // sets / adds the key val pair and returns the map itself
map.set('age', '23')

// .set returns the map iteself, hence we can chain like this
map.set(1,'east').set(2,'west').set(3,'north')


 // this is what a map looks like
 clg(map) 
 // Map(5) {"name" => "sandeep", "age" => "23", 1 => "east", 2 => "west", 3 => "north"}

// Booleans as key
map.set(true, 'open')
map.set(false, 'closed')
console.log(map.get(true)) // open

// to get the value
map.get(key)

//check if map contains a key
map.has(key) // true or false

//get the size
map.size()

// clear
map.clear() //clear all elements

//delete
map.delete(key)


// we can have arrays also as keys so lets try this

map.set([1,2], 'myarray')
console.log(map.get([1,2])) // gives undefined because array is of reference types. Both the references are different

// So do like this

const a = [1, 2]
map.set(a, 'myarray')
console.log(map.get(a)) //myarr


// How to create key val pair without set method in map
const keyval = new Map([
  ['key','value'],
  ['anotherkey','anothervalue']
])


// Convert object to map
const obToMap = new Map(Object.entries(obj))
console.log(obToMap) // map (array of arrays)

//Converting map to array
const arr = [...map]

Building a simple quiz app using Map


const question = new Map([
  ['question', 'which is the best language?'],
  ['correct', 3],
  [1, 'C'],
  [2, 'Java'],
  [3, 'Javascript'],
  [4, 'Python'],
  [true, 'Correct'],
  [false, 'Sorry']
])


console.log(question.get('question'))

for (const [key, value] of question) {
  if (typeof key === 'number')
    console.log(key, value)
}
const userOption = +prompt("Please type your option")
alert(question.get(userOption === question.get('correct')))

14 Summary of JS datastructures and when to use them

15. Strings in Javascript

// contains all string properties and methods

const airline = 'TAP Airline Portugal';
const plane = 'A320';
console.log(plane[0]) // A
console.log(plane[1]) // 3
console.log(plane.length) // 4

// The string methods return new string as the original string can't be mutated

// position of an element - first and last index

console.log(airline.indexOf('l')) // 7 - First appearing l
console.log(airline.lastIndexOf('l')) // 19 - Last appearing l

console.log(airline.indexOf('Portugal')) // 12 - First appearing l
console.log(airline.lastIndexOf('Portugal')) // 12 - Last appearing l

// we get -1 if not found

// Slice 

console.log(plane.slice(1)); // 320A -  displays str starting at index mentioned 
console.log(plane.slice(1, 3)); // 32 - displays str starting at index mentioned till but not including end index

// Using indexOf and slice to get the substrings

// Get the first word of the string
console.log(airline.indexOf(' ')) // 3 -this gives first occurance of ' '. Use this below to get first word
console.log(airline.slice(0, airline.indexOf(' ')))

// similarly, we can get the last word
console.log(airline.slice(airline.lastIndexOf(' ') + 1)) // note we dont need end parameter as we want it to traverse till end

// Get last letter
console.log(airline.slice(-1)) //l

// Get last two letters
console.log(airline.slice(-2)) //al


// We can use the above to get all letters starting at index 1 but last letter
console.log(airline.slice(1, -1)) // AP Airline Portuga

Strings are primitives, so how does it have methods? Only objects contain methods right?

That's right. Behind the scenes, the string primitive gets converted into string object (this process of converting primitive to object is called boxing and the reverse in unboxing). On that string object we have all these methods.

const str = 'sandeep'

gets converted by JS (boxing) to new String(str)

Some more string methods 🤨

// case convertion
const str = "SANDEEp"
console.log(str.toLowerCase()) //sandeep
console.log(str.toUpperCase()) //sandeep

// converting to camel case
const str = "SANDEEp"
console.log(str[0].toUpperCase() + str.slice(1).toLowerCase()) // Sandeep


// comparing 2 emails
const email1 = "hello@gmail.com  "
const email2 = "heLLo@GMail.com"

console.log(email1.trim().toLowerCase() === email2.trim().toLowerCase()) //true

//trim() removes the spaces or newlines
// trimStart() removes spaces at the beginning 
// trimEnd() removes spaces at the end

 
// Replace part of the string
const price = '233,45$'
console.log(price.replace('$', '%')) // 233,45%
console.log(price.replace(',', '.')) // 233.45$ 

// above two can be chained like this
console.log(price.replace('$', '%').replace(',', '.'))

const replaceMe = "I have a cake. I like cake very much"

console.log(replaceMe.replace("cake", "bread")) // replaces first cake

console.log(replaceMe.replaceAll("cake", "bread")) // replaces all cake

// includes, startsWith, endsWith

// does a string contains a character or sub strings
console.log("sandeep".includes('s')) // true
console.log("sandeep".includes('sand')) // true
console.log("sandeep".includes('sanb')) // false

console.log("sandeep".startsWith("san")) //true

console.log("sandeep".endsWith("deep")) //true

Some more string methods please🤔

// split()
console.log("A+very+nice+string".split("+")) // ["A", "very", "nice", "string"]
console.log("Sandeep Amarnath".split(" ")) // ['Sandeep', 'Amarnath]

const [firstName,lastName]="Sandeep Amarnath".split(" ")
// Now let's say we want to add Mr. at the beginning
// we can use join() for this

// join()
const newName = ['Mr.', firstName, lastName].join(" ")
console.log(newName) // Mr. Sandeep Amarnath

// capitalize name
const name = "jessica ann smith david"
const modified = name.split(' ').map(e => e[0].toUpperCase() + e.slice(1)).join(" ")
console.log(modified) // Jessica Ann Smith David


// other way to do the above without using slice but using replace
const name = "jessica ann smith david"
const modified = name.split(' ').map(e => e.replace(e[0], e[0].toUpperCase())).join(" ")
console.log(modified) //// Jessica Ann Smith David

// Padding a string
// padding a string
const message = "Go to gate 23!"
const padStrt = message.padStart(40, "+")
const padEnd = message.padEnd(40, "+")
console.log(padStrt) // ++++++++++++++++++++++++++Go to gate 23!
console.log(padEnd)  // Go to gate 23!++++++++++++++++++++++++++

 // Reapeat
const message = "Go to gate 23! "
console.log(message.repeat(3)) // Go to gate 23! Go to gate 23! Go to gate 23! 

// Concat
console.log("sandeep".concat(" amarnath"," is"," good")) // sandeep amarnath is good 
// which is same as "sandeep" + " amarnath" + " is" + " good"
 

List of string methods we went through

  • indexOf(str)

  • lastIndexOf(str)

  • slice(startIndex) // includes startIndex

  • slice(startIndex,endIndex) // doesnt include end

  • slice(-index) // from last

  • toUpperCase()

  • toLowerCase()

  • includes()

  • startsWith()

  • endsWith()

  • trim()

  • trimStart()

  • trimEnd()

  • replace(element, replaceWithElement) // replaces first occurance

  • replaceAll(element, replaceWithElement) // replaces all occurances

  • split(char) // converts into array seperated when char is found

  • join(char) // convers into string and seperates arr elements by char

  • padStart(count,char)

  • padEnd(count,char)

  • concat(str1,str2...) // similar to + of strings

Note that there is not reverse method on string. You can convert into array by splitting and then reverse it as array supports reverse() and then join back to string

16. Regular expressions in JS 😎

17. Functions

Default parameters

value vs reference types inside a function

First-class functions/citizens and Higher-Order functions

Call-back functions

The functions passed as values and are called at a later stage by another function are called callback functions.

Let's take another example for call-back functions

We know that callback is the function calling another function. Also, we can say, two functions are dependant on each other.

Let's say we have ice-cream shop. To produce the ice-cream, we need to first order the ice-cream. So to production depends on order

PRODUCTION --> Depends on --> ORDER

function order(call_production) {
	console.log("Order placed, please start the production");
	call_production();
}

function production() {
	console.log("Order received. Starting production");
}

order(production);

Call

Apply

Exactly like call method but the second parameter onwards we pass array instead of strings. That is the only difference.

Bind

Similar to call method but, in call method, it directly calls/invokes the method by passing this keyword as first argument. In bind, the function is not called but called-for-later when invoked again. Meaning it returns a function after binding the parameters, so that it can be called at later point in time.

Call, apply and bind have this as the first parameter.

Polyfill bind method

Polyfilling means supporting new features in old browsers. So converting new code/features to the code where old browsers can understand. This conversion is called polyfilling. Now, in the interviews you may be asked to polyfill bind, meaning to implement your own bind method where old browsers can understand.

A must watch video for polyfill bind

Immediately invoked functions (IIFE)

The functions that are invoked immediately are IIFEs.

// Ways to write IIFE is 


(function () {
    console.log("IIFE")
})() // note this must be the first line OR previous line should have ; else this won't work 


!function(){
//code
}()

// AND
~function () {
    console.log("IIFE")
}()

Why do we need IIFE

To make the variables private. In the below example, we see that isPrivate can be accessed only inside the IIFEE but not outside.

In modern JS, we use a block {} instead of IIFE and we can use block because of let and const. let and const are block-scoped whereas var is not. So var cannot be used inside a block.

{
 let isPrivate = true
 var isNotPrivate = true
}

console.log(isPrivate) // Error
console.log(isNotPrivate) // true. var is not private so block scope wont work on var

When to use IIFE?

  1. When you need to avoid polluting global scope and have the code run only once

  2. When you want to use async, await feature

Closures

A function bundled with its lexical environment is called a closure.

A function when returned from another function, even though the outer function's execution context is no longer in the call stack, the returned function still has access to all the variables and functions that existed inside the function before the outer function was removed from call-stack. This is held in a closure.

Using console.dir() we can see closure in console. It is present in [[scopes]]. [[ represents internal property that we.can't access. So we can't access scopes and closure.

A closure is also called a Stateful function because it's the function returned from another function that remembers the state of itself when it was originally present in the function before returning,

Advantages of closures

1. Closures can be used for function currying

Currying can be done in two ways

  • Using bind

  • Using closures

2. Closure can be used for data hiding and encapsulation

3. Closure can be used for Function once ( calling a function only once )

4. Closure can be used for memoization (to keep the time consuming computation in memory and returning it)

Disadvantages of closures

Overconsumption of memory because the closed-over variables are not garbage collected. This might lead to memory leaks and might freeze the browser if not handled properly.

What is a garbage collector?

A program in JS engine that removes or frees up space for unused variables.

Some of the modern browsers like v8 in chrome have smart garbage collection mechanisms. In closed-over-environment, it identifies which variables are not being used by the function which is been returned and has them garbage collected.

Watch crazy Javascript Interview video above and practice the advantages and disadvantages of closures

18 . Async Javascript

What is callback hell, & promises? Resolve CB Hell with promise chaining (.then) and async await. All this you will learn by building an ice-cream shop project.

Reference - https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqa3RydmFHTXdjTnVBRjlsYi1STDBzc2s2T0J0UXxBQ3Jtc0tsdDMydDFZNnVSTkJiajZyY1c4SjdHWERzdFh0VV9YVFpwby1xeTZvU0RubGY0T1NTdkRXT0hLeGowc3U4RVlkRmVxcTVsSWFvZHNjbjhuMDBVSjFVdmFCdlVVMUhoSV80SWF4c0cwYkxmOV9vMGQ3QQ&q=https%3A%2F%2Fwww.freecodecamp.org%2Fnews%2Fjavascript-async-await-tutorial-learn-callbacks-promises-async-await-by-making-icecream%2F

Interview Questions on Promises and SetTimeout

Last updated