JavaScript is a lightweight, Just-in-time compiled, Synchronous Single threaded, Loosly typed programming language.
- Data types
- ECMAScript & ES6
- Most used es6 features
- typeof
- Execution Context and Global Execution Context
- 2 Phases of code run
- Call stack
- Synchronous Single threaded
- Hoisting
- Arrow functions
- Arrow functions hoisting
- Window object
- Global scope
- 'this' keyword
- 'this' context in arrow functions
- Undefined vs not defined vs null
- Scope
- Types of scope
- Lexical scope/environment
- Scope chain
- Lexical this
- let and const
- var vs let vs const
- const with objects
- Temporal dead zone
- Reference vs Syntax vs Type errors
- Declaration vs Initialization
- Arguments vs Parameters
- Default Parameters
- Pass by value vs Pass by reference
- Block scope
- Shadowing
- Clousers
- Uses of Clousers
- SetTimeout
- Data hiding using Clousers
- Disadvantages of Clousers
- Garbage collectors
- Types of functions
- Function statement vs Function expression
- Arguments object
- Function constructors
- Module design pattern and IIFE
- [First class functions/citizens](#44-First-class-functions or citizens)
- Pure and Impure functions
- Recurssion
- Classes
- Callback functions
- Main thread blocking
- Event Listeners
JavaScript provides 2 types of data-types, Primitive
type and Non-Primitive
type.
Primitive type: Data structure that allows you to store only single data type values.
- String
- Number
- BigInt
- Boolean
- Null
- Undefined
- Symbol
NonPrimitive type: Data structure that allows you to store multiple data type values.
- Array
- Object
JavaScript
is a programming language.
ECMAScript
is the specification for JavaScript.
ES6
is the most prominent version of JavaScript, released in the year 2015.
- Let & Const
- Template Literals
- Arrow Functions
- Rest & Spread operators
- Default parameters
- Destructuring assignment
- Classes
- Promises
- Modules
typeof
operator can be used to find the data type of a value.
typeof "superman" // 'string'
typeof "" // 'string'
typeof 100 // 'number'
typeof NaN // 'number'
typeof true // 'boolean'
typeof false // 'boolean'
typeof 1n // 'bigint'
typeof BigInt(9007199254740991) // 'bigint'
typeof function(){} // 'function'
typeof [] // 'object'
typeof {} // 'object'
typeof null // 'object'
typeof new Date() // 'object'
typeof undefined // 'undefined'
typeof mycar // 'undefined'
Everything in JavaScript happens inside an Execution context
. An Execution context is like a container or environment where the JavaScript code evaluated & executed.
A Global execution context
gets created even before it starts to execute any code, so it'll always present at the bottom of the call stack. All the global code i.e. code which is not inside any function or object is executed inside the Global execution context
. Inside the Global execution contex
, a new Execution context
gets created on every time a new function is invoked.
Execution context
is composed of 2 components,
This is the place where all the variables and functions are stored as a key-value pairs.
This is where the code gets executed one line at a time.
A Call stack
is a data structure where data can be pushed & popped and follows the LIFO
( Last in First out ) principle.
In JavaScript, the JavaScript Engine
has its own Call stack
which keeps track of functions to be executed. So whenever a function is invoked, it is pushed into the Call stack
.
JavaScript is a Synchronous Single threaded
language. So when we say Single threaded
that means it can only execute one command at a time.
So when we say Synchronous Single threaded
that means it can move to the next command only after it finishes executing the current line/command.
In plain english Hoist
means raise or lift. In Javascript Hoisting
is the default behaviour of moving all the declarations to the top of their scope before code execution. So if we try to access any variables or functions before their declaration, JS won't throw any error.
In other words, we can also say that Hoisting
is a phenomenon in JS by which we can access variables & functions even before their declaration.
console.log(x) // undefined
console.log(foo()) // hello from foo
var x = "hello"
function foo() {
console.log("hello from foo")
}
Arrow functions
are an alternative to traditional functions, introduced in ES6
. Arrow functions are always Anonymous functions
, so to call them we need to assign it to a variable.
const square = a => a * a;
console.log(square(2)) // 4
using Arrow functions
, curly braces, parenthesis, function keyword & return keywords become optional.
Like traditional functions, arrow functions
are not hoisted
so we can't call them before their declaration.
foo() // Uncaught TypeError: foooo is not a function
bar() // Uncaught ReferenceError: fooo is not defined
var foo = () => {
console.log("hello from foo")
}
const bar = () => {
console.log("hello from bar")
}
Because it behaves just like another variable. It doesn't behave like a function.
Window
is the global object present in the browsers. Every time we run a JS program, the JS engine creates the window
object.
It consists of lot of useful methods & properties and all the global space variables & functions gets attached to the window object so we can access them anywhere in our program.
var a = 10
var b = 20
console.log(window.a) // 10
console.log(window.b) // 20
At global level this
points to window
object.
this === window // true
Anything we write inside JS which is not inside the function or block.
var x = 10 --> global scoped varibale
function foo () { --> global scoped function
var y = 20 --> not global scoped
}
- 'this' keyword refers to the current object
- 'this' keyword always or mostly used in object oriented programming.
- If we get rid of 'this', we will be left with Javscript as a functional programming language.
The 'this' keyword refers to different objects depending on how it is used:
In an object method, 'this' refers to the object.
const person = {
fname: 'neelesh',
lname: 'shetty',
fullName: function() {
return this.fname + ' ' + this.lname; ----> 'this' refers to 'person' object itself.
}
}
person.fullName() // neelesh shetty
let x = this.
console.log(x) // Window {window: Window, self: Window, document: document, name: 'iframeResult', location: Location, …}
when used alone, this refers to global object
. Becoz 'this' is in global scope.
In a browser window the global object is [object Window].
In strict mode:
'use strict'
let y = this.
console.log(y) // Window {window: Window, self: Window, document: document, name: 'iframeResult', location: Location, …}
In a function, it refers to global object
.
function hello() {
console.log(this)
}
hello() // Window {window: Window, self: Window, document: document, name: 'iframeResult', location: Location, …}
In strict mode:
'use strict'
function hey() {
console.log(this)
}
hey() // undefined
When a function is used as an event handler, its 'this' is set to the element on which the listener is placed.
<button onclick="this.style.display='none'">Click to remove button</button>
so when we click the button, the above style will get added.
An arrow function expression is a compact alternative to a traditional function expression.
-
Using the arrow function, curly braces, paranthesis, function & return keywords become optional.
-
Arrow functions are always Anonymous functions, so to call arrow function we need to assign it to a variable.
-
Arrow function expressions should only be used for non-method functions because they do not have their own
this
.const obj = { fname: 'neel', lname: 'shett', getName: () => console.log(this.fname, this) ---> 'this' points to the global window object. } obj.getName() // undefined, Window {window: Window, self: Window, document: document, name: 'iframeResult', location: Location, …}
-
Arrow functions do not have arguments binding unlike regular functions.
const x = () => { console.log(arguments); } x(4,6,7) // ReferenceError: Can't find variable: arguments
but with regular function,
let x = function () { console.log(arguments); } x(4,6,7); // Arguments [4, 6, 7]
To solve this issue, we can use
spread operator
:let x = (...args) => { console.log(args); } x(4,6,7); // [4, 6, 7]
-
Arrow functions provide better syntax to write
promises & callbacks
.asyncFunction().then(function() { return asyncFunction1(); }).then(function() { return asyncFunction2(); }).then(function() { finish; });
asyncFunction() .then(() => asyncFunction1()) .then(() => asyncFunction2()) .then(() => finish);
-
You cannot use an arrow function as a
constructor
.let Foo = () => {}; let foo = new Foo(); // TypeError: Foo is not a constructor
undefined
keyword or property indicates that a variable has not been assigned/initialized a value. undefined
variables takes up their own memory in the memory space.
not defined
property indicates that a variable is not at all present in the code and in the memory space.
null
is an object in JavaScript. null
means nothing & is used to represent an intentional absence of value.
Scope
defines the visibility & availability of variables and functions, meaning it's a place where a defined variable/function can have its existence and beyond that it cannot be accessed.
In JS, we can create scope
using code blocks, functions & modules.
There are 3 types of scope in JS:
Any variable that is not inside any function or block, is inside the global scope & they are called global scoped variables.
The variables in global scope
can be accessed from anywhere in the program.
var message = "hello"
function printHello() {
console.log(message)
}
printHello() // "hello"
When a variable is declared inside a function, it is only accessible within that function and cannot be used outside that function
var
is a function scoped, becoz:
function hello() {
if (true) {
var a = 1; --- function scoped
let b = 2; --- block scoped
const c = 3; -- block scoped
console.log(a);
console.log(b);
console.log(c);
}
console.log(a);
console.log(b);
console.log(c);
}
hello();
output:
1
2
3
1
Reference error: b is not defined.
So we can access var
throughout the function.
A variable when declared inside the if or switch conditions or inside for or while loops, are accessible within that particular condition or loop.
As you can see in the above hello
example, we cannot accesss b
or c
outside its if block.
Lexical scoping is the environment that holds the variables of the current scope as well as the outer scope.
or,
Lexical Environment is the local memory along with the lexical environment of its parent.
or,
Lexical scoping is a type of object oriented programming according to which, a child can access parent scope and global scope.
It is the process in which, JavaScript engine searches for the value of the variables in the scope of the functions. However, the search is in a lexical manner.
First of all the engine looks out in the current scope of the current function. If not found, it finds it in the parent funtion. If not there, global scope is the last place it checks in.
Hence, to find value of the required variabe, a chain is formed by looking in the different scopes.
const a = "Hello world"; -- global scope variable
function first() {
const b = "I am Neelesh."; -- parent scope variable
second();
function second() {
const c = "A frontend developer"; -- current/local scope variable
console.log(a + b + c);
}
}
first();
// Hello world I am Neelesh. A frontend developer
Arrow functions do not have their own value of this. The value of this in an arrow function is inherited from the enclosing (lexical) scope.
It is just a fancy way of saying its value is static and determined by the scope "this" is defined in.
const myFunction = () => {
console.log(this);
};
myFunction() // window
- Both are introduced in ES6.
- Variables defined with
let
can be Reassigned. - Variables defined with
const
cannot be Reassigned.
let & const are also hoisted, but very deferently than var
declaration.
console.log(b) //undefined
console.log(a) //Reference error: a is not defined
let a = 10
var b = 100
Here, b
gets its memory allocated in the global scope becoz of var
.
But, a
gets its memory allocated in the seperate memory space called Script
. So we can't access let & const before it gets its value.
Note: I tried the above code in different places like,
- codesandbox.io => output is: // undefined.
- nodejs => output is: // ReferenceError: Cannot access 'a' before initialization.
- browser console => output is: // ReferenceError: 'a' is not defined.
- browser console with index.js => output is: // ReferenceError: Cannot access 'a' before initialization.
var | let & const |
---|---|
1. var is a function scoped. | 1. let & const are block scoped. |
2. Variables defined with var can be redeclared & | 2. let can be reasigned but cannot be redeclared. |
reasigned. | const cannot be reasigned or redeclared. |
3. It can be accessed without initialization as | 3. It cannot be accessed without initialization |
its default value is “undefined”. | otherwise it will give ‘referenceError’. |
Const
allows us to change the Object properties.
const obj = { name: 'Bob' }
obj.name = 'silly'
console.log(obj) // { name: 'silly' }
So, const
only prevent reassigning of primitive data types.
The time/period b/w let
or const
variable is hoisted & till it initialised some value.
So until it gets some values, the let
and const
will be in temporal dead zone.
When there is no variable found in the memory.
When there are mistakes in the source code, such as spelling and punctuation errors, incorrect labels, and so on.
when a value is not of the expected type.
Ex: if a string is attempted to be multiplied with an integer, a TypeError
is generated.
Declaration
declares the creation of variables and functions. or just naming the variable.
Initialization
occurs when you assign an initial value to a variable.
Arguments
are the values that are paased to the function during the function call.
Parameters
are the variables that are used to recieve the arguments that are passed to the function.
function printName (name, city) {. ---- Parameters
return `Iam {name} from {city}`;
}
printName('Neelesh', 'Ckm') ---- Arguments
Default parameters
allow named parameters to be initialized with default values if no value or undefined is passed.
function multiply(a, b = 1) { --- 'b' has been initialized with a default value 1.
return a * b;
}
console.log(multiply(5, 2));
// Expected output: 10
console.log(multiply(5));
// Expected output: 5
When a function is called, the arguments can be passed in two ways, either Pass by value or Pass by reference (address).
Primitive data types such as string, number, null, undefined, and boolean, are passed by value while non-primitive data types such as objects, arrays, and functions are passed by reference.
Pass by value
in JavaScript means that a copy of the actual parameter’s value is made in memory. The original value and the copied value are independent of each other as they both have a different space in memory i.e., on changing the value inside the function, the variable outside the function is not affected.
Unlike pass by value, pass by reference
in does not create a new space in the memory, instead, we pass the reference/address of the actual parameter. Thus, if we change the value of the variable inside the function, then the original value also gets changed.
We use block
to combine multiple statements.
{
---- this is a Block.
}
Ex: if else statements, for loops etc.
shadowing
occurs when a variable declared in a certain scope (e.g. a local variable) has the same name as a variable in an outer scope (e.g. a global variable). When this happens, the outer variable is said to be shadowed by the inner variable.
var a = 100;
{
var a = 20;
}
console.log(a) // 20
First it assigns 100 to a
& then when it comes at block scoped a
, it again assigns 20 to the same a
in the memory. So, value of a
is 20.
let b = 100;
{
let b = 20;
}
console.log(a) // 100
Becoz, let
is block scoped & shadowing won't happen. Same foe const
.
Note: It actually creates 2 b
variables, one in Global
& another in Block
.
let c = 10;
{
var c = 20; ---- illigal shadowing.
}
console.log(a) // SyntaxError: Identifier 'a' has already been declared
var a = 100
{
let a = 20 ---- legal
}
console.log(a) // 100
Closure
is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
or,
Closure gives you access to an outer function's scope from an inner function.
or,
Function along with its lexical scope forms a closure
.
function outer () {
const a = 7;
function inner () {
console.log(a)
}
return inner;
}
const z = outer();
.
...million lines of code
.
z() // 7
So, even if we execute the returned function outside its scope, it still remembers its variables with the help of clousers
.
function x() {
let a = 900;
function y() {
let b = 7;
function z() {
console.log(a, b) Closure:(z)
} b: 900
return z; Closure:(y)
} a: 7
return y;
}
const a = x() or just x()()() // 900, 7
const b = a()
b() // 900, 7
Note: Clousers remembers the reference to the variables & not the actual value.
- Module design pattern
- Currying
- Functions like once
- memoize
- Maintaining state in async world
- SetTimeouts
- Iterators etc.
- Overconsumption of memory becoz variables are not
garbage collected
. - If not handled properly then
memory leak
could happen.
JavaScript doesn't wait for none.
function x() {
const a = 1;
setTimeout(() => {
console.log(a)
}, [3000])
console.log('hi')
}
x()
//'hi'
// 1
Consider this counter function,
let count = 0
function counter() {
counter++
}
counter1()
But if we want to prevent any function accessing the count
variable, we wrap this with another function like below.
function counter() {
let count = 0
return function increment() {
count++
return count
}
}
const counter1 = counter();
counter1() // 1
counter1() // 2
counter1() // 3
By this method, the variables are safe & also we can create any number of counter instances like counter2 = counter(), this will be a new counter which starts from 0.
If there is any unused variable in the memory, those are garbage collected
.
A named function is a Function statement
function a() {
//.....
}
This is same as function statement but we can omit the function name.
const a = function () {
//....
}
Function statement which doesn't have a name or identity.
function () {
//....
}
Note: you can't call these type of functions & it gives SynaxError.
const a = function xyz() {
console.log('hi')
}
a() // hi
xyz() // ReferenceError: xyz is not defined.
So, we can't access xyz outside of that function, becoz it is created as a local scope. Try console logging xyz inside the xyz function itself & you will get the output of the whole xyz function.
Major difference is Hoisting
.
a() // 'statement' ---> a: fn{..}
b() // b is not a function ---> b: undefined
function a() {
console.log('statement')
}
const b = function() {
console.log('expression')
}
So, function statements are hoisted as a variables.
The arguments of a function are maintained in an array-like object. Within a function, you can address the arguments passed to it as follows:
arguments[i];
function myConcat(separator) {
let result = ""; // initialize list
// iterate through arguments
for (let i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}
myConcat("; ", "elephant", "giraffe", "lion", "cheetah") // "elephant; giraffe; lion; cheetah; "
The Function() constructor creates a new Function object. Calling the constructor directly can create functions dynamically, but suffers from security and similar (but far less significant) performance issues as eval() becoz we are trying to evaluate string as Javascript.
Function constructor creates functions which execute in the global scope only.
const sum = new Function('a', 'b', 'return a + b');
sum(2, 6) // 8
Note: Function() can be called with or without new. Both create a new Function instance.
Warning: This type of methods are not recommended.
The module pattern is a design pattern used for improving the maintainability and reusability of the code by creating public
and private
access levels.
Sometimes called encapsulation
, it protects the value inside a module from being accessed from other scopes.
A programming language is said to have First-class
functions when functions in that language are treated like any other variable.
For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
const foo = () => {
console.log("foobar");
};
foo(); // Invoke it using the variable
function greeting(helloMessage, name) {
console.log(helloMessage() + name);
}
function sayHello() {
return "Hello, ";
}
// Pass `sayHello` as an argument to `greeting` function
greeting(sayHello, "JavaScript!");
// Hello, JavaScript!
function sayHello() {
return () => {
console.log("Hello!");
};
}
A pure function is a function that returns the same result if the same arguments(input) are passed to the function.
-
The return value of the function on the function call should only be dependent on the input function arguments.
-
It should not modify any non-local state. It means the function should not manipulate anything other than the data stored in the local variables declared within the function.
function operationAdd(a, b){ // A pure function adding two integers passed in it.
return a+b;
}
function operationDivide(a, b){ // Pure function to divide two integers passed in it.
return a/b;
}
function operationMulti(a, b){ // Pure function to multiple two integers passed in it.
return a*b;
}
console.log( // Calling all the pure functions
operationAdd(2,5),
operationMulti(3,2),
operationDivide(20,5)
);
-
Better Readability: Pure functions increase the readability of the javascript code because of its simplicity.
-
Testable: It's easier to perform unit testing on a pure function than on an impure function. A pure function result only depends upon its input arguments hence the whole function can be treated as an independent entity. So we can just take that function and test it with some standard input values whose output is known.
-
Better Performance: Pure functions can be memoized and this makes your application faster.
An impure function gives inconsistent results or different results for the same input values.
function impureFunc(myArray, item) {
return myArray.push(item);
}
In the above example, we are manipulating the myArray argument and a pure function should not change(manipulate) any part of the code. That's why this is an impure function.
const message = 'Hi there! ';
function myMessage(value) {
return `${message} ${value}`
}
console.log(myMessage('hello'));
In the above code, the result the function is returning is dependent on the variable that is not declared inside the function, that's why this is an impure function.
- Making an HTTP request
- Mutating data
- Printing to a screen or console
- DOM Query/Manipulation
- Using Math.random()
- Getting the current time
Recursion is a process of calling itself. A function that calls itself is called a recursive function.
- A recursive function must have a condition to stop calling itself. Otherwise, the function is called
indefinitely
. - Once the condition is met, the function stops calling itself. This is called a
base condition
.
// program to find the factorial of a number
function factorial(x) {
if (x < 0) return 'Invalid input'
if (x === 0) {
return 1;
} else {
return x * factorial(x - 1);
}
}
factorial(3) // 6
Classes are a template for creating objects.
- Classes were introduced in
EcmaScript 2015 (ES6)
to provide a cleaner way to follow object-oriented programming patterns. - JavaScript still follows a prototype-based inheritance model. Classes in JavaScript are syntactic sugar over the prototype-based inheritance model which we use to implement OOP concepts.
- Thus the introduction of classes in JS made it easier for developers to build software around OOP concepts. It also brought in similarities to different OOP-based programming languages such as C++ and Java.
- Before classes, we used constructor functions to do OOP in JavaScript. Have a look at the example below:
function Pen(name, color, price) {
this.name = name;
this.color = color;
this.price = price;
}
const pen1 = new Pen("Marker", "Blue", "$3");
// Adding function in a constructor
Pen.prototype.showPrice = function(){
console.log(`Price of ${this.name} is ${this.price}`);
}
pen1.showPrice();
We can re-create the above example much cleaner with the help of the Class keyword:
class Pen {
constructor(name, color, price){
this.name = name;
this.color = color;
this.price = price;
}
showPrice(){
console.log(`Price of ${this.name} is ${this.price}`);
}
}
const pen1 = new Pen("Marker", "Blue", "$3");
pen1.showPrice();
A callback function is a function passed into another function as an argument.
The benefit of using a callback function is that we can wait for the result of a previous function call and then execute another function call.
function greeting(name) {. --> callback function
alert(`Hello, ${name}`);
}
function processUserInput(cb) {
const name = prompt("Please enter your name.");
cb(name);
}
processUserInput(greeting);
The above example is a synchronous
callback, as it is executed immediately.
However, callbacks are often used to continue code execution after an asynchronous
operation has completed.
A good example is the callback functions executed inside a .then()
block chained onto the end of a promise after that promise fulfills or rejects.
The browser uses a single thread to run all the JavaScript in your page, as well as to perform layout, reflows, and garbage collection.
This means that long-running JavaScript functions can block the thread, leading to an unresponsive page and a bad user experience.
For example: An image transformation which needs 10sec to be computed, you are blocking the UI responses (ex. JS animations, clicks, inputs, typing, etc.)
So the browser itself could decide to take action and show a popup to the user asking whether to kill that process
or keep it running.
- Create a WebWorker: It is an API for running JavaScript code in a different browser’s thread. Its limitation is it has no access to the DOM. It can communicate with the main thread only via messages.
It is useful for running your computation outside the main thread and once finished getting the response for it. Though if your code needs frequent access to read and manipulate the DOM, maybe this is not the best option.
- Slice your long-task in little sub-tasks and run them asynchronously: We can use the setTimeout API for this and take advantage of the Event queue logic to have other things (Job queue and Rendering).
An event listener is a function that runs once a specific event occurs. So, an event listener “listens” for an action, then calls a function that performs a related task.
You can listen for events on any element in the DOM. JavaScript has an addEventListener() function that you can call on any element on a web page.
document.querySelector('.btn').addEventListener("click", clickDemo)
function clickDemo(){
console.log("Hi there")
}
Event-driven programming
is the name of a paradigm that relies on the execution of an event to perform its functions.