JavaScript Basics
See the codeClick ★ if you like the project. Your contributions are heartily ♡ welcome.
Focus: Syntax, basic logic, and standard data handling.
Focus: ES6+ features, DOM, and common "trick" concepts.
Focus: Asynchronous operations, performance, and internal engine mechanics. [4]
Focus: Scalability, security, and low-level optimization.
Focus: Code review, team standards, architectural decisions, and engineering best practices for leading a development team.
Focus: System-level design, scalability, security posture, micro-frontend, and cross-team JavaScript architecture.
1. Template Strings:
Template literals are string literals allowing embedded expressions.
Benefits:
// String Substitution
let name = `Abhinav Sharma`;
console.log(`Hi, ${name}`); // Output: "Hi, Abhinav Sharma"
// Multiline String
let msg = `Hello \n
World`;
console.log(`${msg}`); // Output: "Hello World"
⚝ Try this example on CodeSandbox
2. Spread Operator:
Spread operator allows iterables( arrays / objects / strings ) to be expanded into single arguments/elements.
function sum(x, y, z) {
return x + y + z;
}
const numbers = [10, 20, 30];
// Using Apply (ES5)
console.log(sum.apply(null, numbers)); // 60
// Using Spread Operator
console.log(sum(...numbers)); // 60
⚝ Try this example on CodeSandbox
2.1. Copying an array:
const fruits = ["Apple", "Orange", "Banana"];
const newFruitArray = [...fruits];
console.log(newFruitArray);
Output:
['Apple', 'Orange', 'Banana']
⚝ Try this example on CodeSandbox
2.2. Concatenating arrays:
const arr1 = ["A", "B", "C"];
const arr2 = ["X", "Y", "Z"];
const result = [...arr1, ...arr2];
console.log(result);
Output:
['A', 'B', 'C', 'X', 'Y', 'Z']
⚝ Try this example on CodeSandbox
2.3. Spreading elements together with an individual element:
const fruits = ["Apple", "Orange", "Banana"];
const newFruits = ["Cherry", ...fruits];
console.log(newFruits);
Output:
['Cherry', 'Apple', 'Orange', 'Banana']
⚝ Try this example on CodeSandbox
2.4. Spreading elements on function calls:
const fruits = ["Apple", "Orange", "Banana"];
const getFruits = (f1, f2, f3) => {
console.log(`Fruits: ${f1}, ${f2} and ${f3}`);
};
getFruits(...fruits);
Output:
Fruits: Apple, Orange and Banana
⚝ Try this example on CodeSandbox
2.5. Spread syntax for object literals:
const obj1 = { id: 101, name: 'Rajiv Sandal' }
const obj2 = { age: 35, country: 'INDIA' }
const employee = { ...obj1, ...obj2 }
console.log(employee);
Output:
{
"id": 101,
"name": "Rajiv Sandal",
"age": 35,
"country": "INDIA"
}
3. Sets:
Sets are a new object type with ES6 (ES2015) that allow to create collections of unique values. The values in a set can be either simple primitives like strings or integers, but more complex object types like object literals or arrays can also be part of a set.
const numbers = new Set([10, 20, 20, 30, 40, 50]);
console.log(numbers); Set(5) { 10, 20, 30, 40, 50 }
console.log(typeof numbers); // Object
⚝ Try this example on CodeSandbox
4. Default Parametrs:
function add(x = 10, y = 20) {
console.log(x + y);
}
add(10, 30); // Output: 10 + 30 = 40
add(15); // Output: 15 + 20 = 35
add(); // Output: 10 + 20 = 30
⚝ Try this example on CodeSandbox
5. repeat():
The repeat() method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together.
const msg = "Hello World \n";
console.log(`${msg.repeat(3)}`);
Output:
Hello World
Hello World
Hello World
⚝ Try this example on CodeSandbox
6. Arrow Function (=>):
const add = (x, y) => x + y;
console.log(add(10, 20)); // 30
⚝ Try this example on CodeSandbox
7. Arrow function with this
/**
* Using Arrow function
*/
const person = {
name: "Diksha",
actions: ["bike", "hike", "ski"],
printActions() {
this.actions.forEach((action) => {
console.log(this.name + " likes to " + action);
});
},
};
person.printActions();
Output:
Diksha likes to bike
Diksha likes to hike
Diksha likes to ski
⚝ Try this example on CodeSandbox
8. Destructing Assignment:
// Destructing Assignment
const { title, price, description } = {
title: "iPhone",
price: 999,
description: "The iPhone is a smartphone developed by Apple"
};
console.log(title); // iPhone
console.log(price); // 999
console.log(description); // The iPhone is a smartphone developed by Apple
⚝ Try this example on CodeSandbox
9. Generators:
A generator is a function that can stop midway and then continue from where it stopped. In short, a generator appears to be a function but it behaves like an iterator.
function* generator(num) {
yield num + 10;
yield num + 20;
yield num + 30;
}
const gen = generator(10);
console.log(gen.next().value); // 20
console.log(gen.next().value); // 30
console.log(gen.next().value); // 40
⚝ Try this example on CodeSandbox
10. Symbols:
They are tokens that serve as unique IDs. We create symbols via the factory function Symbol(). Symbols primary use case is for making private object properties, which can be only of type String or Symbol (Numbers are automatically converted to Strings).
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol("Hi");
console.log(typeof symbol1); // symbol
console.log(symbol3.toString()); // Symbol(Hi)
console.log(Symbol("Hi") === Symbol("Hi")); // false
⚝ Try this example on CodeSandbox
11. Iterator:
The iterable is a interface that specifies that an object can be accessible if it implements a method who is key is [symbol.iterator].
const title = "ES6";
const iterateIt = title[Symbol.iterator]();
console.log(iterateIt.next().value); //output: E
console.log(iterateIt.next().value); //output: S
console.log(iterateIt.next().value); //output: 6
⚝ Try this example on CodeSandbox
Global variables are declared outside of a function or declared with a window object for accessibility throughout the program (unless shadowed by locals). If you declare a variable without using var, even if it's inside a function, it will still be seen as global.
The var statement declares a function-scoped or globally-scoped variable, optionally initializing it to a value.
Example:
var x = 10;
if (x === 10) {
var x = 20;
console.log(x); // expected output: 20
}
console.log(x); // expected output: 20
Example: Declaring global variable within function
window.value = 90;
// Declaring global variable by window object
function setValue() {
window.value = 100;
}
// Accessing global variable from other function
function getValue() {
setValue();
return window.value;
}
console.log(getValue()); // 100
Using Undeclared Variables:
let, const, or var, you'll end up creating a new global variable. It will be global no matter how deeply nested within functions and blocks your code is, which is almost certainly not what you want, is bug-prone, and is one of the best reasons for using strict mode!var: they define properties of the global object. But unlike the properties defined by proper var declarations, these properties can be deleted with the delete operator.⚝ Try this example on CodeSandbox
Template literals help make it simple to do string interpolation, or to include variables in a string.
const person = { name: 'Tyler', age: 28 };
console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`);
// 'Hi, my name is Tyler and I am 28 years old!'
Template literals, however, preserve whatever spacing you add to them. For example, to create that same multi-line output that we created above, you can simply do:
console.log(`This is line one.
This is line two.`);
// This is line one.
// This is line two.
Another use case of template literals would be to use as a substitute for templating libraries for simple variable interpolations:
const person = { name: 'Tyler', age: 28 };
document.body.innerHTML = `
<div>
<p>Name: ${person.name}</p>
<p>Name: ${person.age}</p>
</div>
`
let, var or const?Variables declared using the var keyword are scoped to the function in which they are created, or if created outside of any function, to the global object. let and const are block scoped, meaning they are only accessible within the nearest set of curly braces (function, if-else block, or for-loop).
/**
* All variables are accessible within functions.
*/
function variableScope() {
var x = 10;
let y = 20;
const z = 30;
console.log(x); // 10
console.log(y); // 20
console.log(z); // 30
}
console.log(x); // ReferenceError: x is not defined
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
variableScope();
/**
* var declared variables are accessible anywhere in the function scope.
*/
if (true) {
var a = 10;
let b = 20;
const c = 30;
}
console.log(a); // 10
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
var allows variables to be hoisted, meaning they can be referenced in code before they are declared. let and const will not allow this, instead throwing an error.
console.log(a); // undefined
var a = 'foo';
console.log(b); // ReferenceError: can\'t access lexical declaration 'b' before initialization
let b = 'baz';
console.log(c); // ReferenceError: can\'t access lexical declaration 'c' before initialization
const c = 'bar';
Redeclaring a variable with var will not throw an error, but 'let' and 'const' will.
var a = 'foo';
var a = 'bar';
console.log(a); // "bar"
let b = 'baz';
let b = 'qux'; // Uncaught SyntaxError: Identifier 'b' has already been declared
let and const differ in that let allows reassigning the variable's value while const does not.
// This is ok.
let a = 'foo';
a = 'bar';
console.log(a); // bar
// This causes an exception.
const b = 'baz';
b = 'qux';
console.log(b) // TypeError: Assignment to constant variable.
⚝ Try this example on CodeSandbox
The double exclamation mark (!!) in JavaScript forces any value into its corresponding strict boolean representation (true or false). The !! (double negation) operator converts any value to its boolean equivalent:
! negates the value (converting it to a boolean)! negates it again, giving the true boolean representationExample:
console.log(!!0); // false
console.log(!!""); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!!1); // true
console.log(!!"text"); // true
console.log(!!{}); // true
console.log(!![]); // true
Note: It's commonly used to explicitly cast a value to boolean without using Boolean(value).
var x = 1 and x = 1?The primary difference is that var x = 1 explicitly declares a function-scoped or globally-scoped variable, whereas x = 1 assigns a value to an identifier without declaring it, which can lead to global scope pollution or runtime errors depending on your code's strictness mode
var x = 1:
var inside a { } block can be accessed from outside the block.var inside a function are not accessible (visible) from outside the function.var will not trigger an error, even in strict mode, and the variable will not lose its value unless another assignment is performed.Example:
var x = 1;
if (x === 1) {
var x = 2;
console.log(x); // expected output: 2
}
console.log(x); // expected output: 2
var x = 5; // global
function someThing(y) {
var x = 3; // local
var z = x + y;
console.log(z);
}
someThing(4); // 7
console.log(x); // 5
x = 1:
x = 1 is accessible in: (Block scope - Function scope - Global scope)let, const, or var, you'll end up creating a new global variable. It will be global no matter how deeply nested within functions and blocks your code is, which is almost certainly not what you want, is bug-prone, and is one of the best reasons for using strict mode!var they define properties of the global object.var declarations, these properties can be deleted with the delete operator.Example:
var x = 5; // global
function someThing(y) {
x = 1; // still global!
var z = x + y;
console.log(z);
}
someThing(4) // 5
console.log(x) // 1
Example:
{
console.log(x + y); // NaN
var x = 1;
var y = 2;
}
{
console.log(x + y); // Uncaught ReferenceError: x is not defined
x = 1;
y = 2;
}
| var x = 1 | x = 1 | |
|---|---|---|
| Strict mode | ✔ | ❌ |
| Block scope | ❌ | ✔ |
| Function scope | ✔ | ✔ |
| Global scope | ✔ | ✔ |
| Hoisting | ✔ | ❌ |
| Reassigning | ✔ | ✔ |
In JavaScript, you can assign default values to variables using several modern and legacy approaches, depending on the exact conditions you want to check.
1. OR operator (||)
let name = userInput || "Default";
// Uses "Default" if userInput is falsy (0, "", null, undefined, false)
2. Nullish coalescing (??) (preferred)
let name = userInput ?? "Default";
// Uses "Default" only if userInput is null or undefined (not 0 or "")
3. Destructuring defaults
const { name = "John", age = 25 } = user;
const [first = 0, second = 0] = arr;
4. Function parameter defaults
function greet(name = "World") {
return `Hello, ${name}!`;
}
greet(); // "Hello, World!"
greet("Alice"); // "Hello, Alice!"
5. Logical OR assignment (||=)
let x = null;
x ||= "default"; // x = "default"
6. Nullish assignment (??=)
let x = 0;
x ??= "default"; // x stays 0 (not null/undefined)
Key difference between || and ??:
let count = 0;
count || 10; // 10 — 0 is falsy, so falls back
count ?? 10; // 0 — 0 is not null/undefined, so keeps it
Note: Use ?? when 0, "", or false are valid values.
Local variables always take precedence over global variables within their scope.
let x = "global";
function example() {
let x = "local";
console.log(x); // "local" — local shadows the global
}
example();
console.log(x); // "global" — unchanged
Scope chain — JavaScript looks up variables from innermost to outermost:
let x = "global";
function outer() {
let x = "outer";
function inner() {
let x = "inner";
console.log(x); // "inner"
}
inner();
console.log(x); // "outer"
}
outer();
console.log(x); // "global"
If no local variable exists, it walks up the chain:
let x = "global";
function example() {
console.log(x); // "global" — no local x, found in outer scope
}
Shadowing pitfall:
let x = "global";
function example() {
console.log(x); // ReferenceError (TDZ) — let/const are hoisted but not initialized
let x = "local";
}
Precedence order (innermost → outermost):
window / globalThis)⚝ Try this example on CodeSandbox
Variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope. This outer variable is said to be shadowed.
If there is a variable in the global scope, and you'd like to create a variable with the same name in a function. The variable in the inner scope will temporarily shadow the variable in the outer scope.
Example:
let x = "global";
function example() {
let x = "local"; // shadows the global x
console.log(x); // "local"
}
example();
console.log(x); // "global" — outer x is unaffected
Block-level shadowing (let/const):
let msg = "outside";
if (true) {
let msg = "inside"; // shadows outer msg
console.log(msg); // "inside"
}
console.log(msg); // "outside"
var does NOT shadow at block level (only at function level):
var msg = "outside";
if (true) {
var msg = "inside"; // overwrites, not shadows!
console.log(msg); // "inside"
}
console.log(msg); // "inside" — original was overwritten!
Nested function shadowing:
let value = 1;
function outer() {
let value = 2; // shadows global
function inner() {
let value = 3; // shadows outer
console.log(value); // 3
}
inner();
console.log(value); // 2
}
outer();
console.log(value); // 1
Note:
let/const shadow at block level; var only at function levelThe scope chain is the mechanism JavaScript uses to resolve variable references by searching through nested scopes from innermost to outermost until the variable is found (or a ReferenceError is thrown).
Example::
let globalVar = "I'm a global variable";
function outer() {
let outerVar = "I'm an outer variable";
function inner() {
let innerVar = "I'm an inner variable";
console.log(innerVar); // "I'm an inner variable"
console.log(outerVar); // "I'm an outer variable"
console.log(globalVar); // "I'm a global variable"
}
inner();
}
outer();
Note:
There are eight basic data types in JavaScript.
| Data Types | Description | Example |
|---|---|---|
| String | Represents textual data | let str = 'Hi', let str2 = "Hello", let str3 = `Hello World` |
| Number | An integer or a floating-point number | let num = 3, let num2 = 3.234, let num3 = 3e-2 |
| BigInt | An integer with arbitrary precision | let num = 900719925124740999n, let num = 1n |
| Boolean | Any of two values: true or false | let flag = true |
| undefined | A data type whose variable is not initialized | let a; |
| null | Denotes a null value | let a = null; |
| Symbol | Data type whose instances are unique and immutable | let value = Symbol('hello'); |
| Object | key-value pairs of collection of data | let student = { }; |
String:
String is used to store text. In JavaScript, strings are surrounded by quotes:
Example::
// Strings
const firstName = "Mukul";
const lastName = "Mittal";
const result = `Name: ${firstName} ${lastName}`;
console.log(result); // Name: Mukul Mittal
console.log(typeof result); // string
Number:
Number represents integer and floating numbers (decimals and exponentials). A number type can also be +Infinity, -Infinity, and NaN (not a number).
const number1 = 3;
const number2 = 3.433;
const number3 = 3e5; // 3 * 10^5
const number4 = 3 / 0;
console.log(number4); // Infinity
console.log(typeof number4) // number
const number5 = -3 / 0;
console.log(number5); // -Infinity
console.log(typeof number5); // number
// strings can\'t be divided by numbers
const number6 = "abc" / 3;
console.log(number6); // NaN
console.log(typeof number6); // number
BigInt:
In JavaScript, Number type can only represent numbers less than (253 - 1) and more than -(253 - 1). However, if you need to use a larger number than that, you can use the BigInt data type.
A BigInt number is created by appending n to the end of an integer.
// BigInt value
const num1 = 100000000000000000n;
const num2 = 1000000000000000000n;
const num3 = 10;
// Adding two big integers
const result1 = num1 + num2;
console.log(result1); // "1100000000000000000n"
console.log(typeof result1); // bigint
// Error! BitInt and number cannot be added
const result2 = num1 + num2 + num3;
console.log(result2); // Uncaught TypeError: Cannot mix BigInt and other types
console.log(typeof result2); // Uncaught TypeError: Cannot mix BigInt and other types
Boolean:
This data type represents logical entities. Boolean represents one of two values: true or false.
const dataChecked = true;
const valueCounted = false;
undefined:
The undefined data type represents value that is not assigned. If a variable is declared but the value is not assigned, then the value of that variable will be undefined.
let name;
console.log(name); // undefined
let name = undefined;
console.log(name); // undefined
null:
In JavaScript, null is a special value that represents empty or unknown value.
const number = null;
Symbol:
A value having the data type Symbol can be referred to as a symbol value. Symbol is an immutable primitive value that is unique.
// Two symbols with the same description
const value1 = Symbol('hello');
const value2 = Symbol('hello');
let result = (value1 === value2) ? true : false; // false;
// Note: Though value1 and value2 both contain 'hello', they are different as they are of the Symbol type.
Object:
An object is a complex data type that allows us to store collections of data.
const employee = {
firstName: 'John',
lastName: 'K',
email: 'john.k@gmail.com'
};
undefined property?The undefined property indicates that a variable has not been assigned a value, or not declared at all. The type of undefined value is undefined too.
var user; // Value is undefined, type is undefined
console.log(typeof(user)) //undefined
Any variable can be emptied by setting the value to undefined.
user = undefined
null vs undefined?Null:
Null means an empty or non-existent value. Null is assigned, and explicitly means nothing.
var test = null;
console.log(test); // null
null is also an object. Interestingly, this was actually an error in the original JavaScript implementation:
console.log(typeof test); // object
Undefined:
Undefined means a variable has been declared, but the value of that variable has not yet been defined. For example:
var test2;
console.log(test2); // undefined
Unlike null, undefined is of the type undefined:
console.log(typeof test2); // undefined
Difference:
| Null | Undefined |
|---|---|
| It is an assignment value which indicates that variable points to no object. | It is not an assignment value where a variable has been declared but has not yet been assigned a value. |
| Type of null is object | Type of undefined is undefined |
| The null value is a primitive value that represents the null, empty, or non-existent reference. | The undefined value is a primitive value used when a variable has not been assigned a value. |
| Indicates the absence of a value for a variable | Indicates absence of variable itself |
| Converted to zero (0) while performing primitive operations | Converted to NaN while performing primitive operations |
Type coercion is the automatic or implicit conversion of values from one data type to another (such as strings to numbers). Type conversion is similar to type coercion because they both convert values from one data type to another with one key difference — type coercion is implicit whereas type conversion can be either implicit or explicit.
Example:
const value1 = '10';
const value2 = 20;
let sum = value1 + value2;
console.log(sum);
In the above example, JavaScript has coerced the 10 from a number into a string and then concatenated the two values together, resulting in a string of 1020. JavaScript had a choice between a string or a number and decided to use a string.
// Example of explicit coercion
const value1 = '10';
const value2 = 20;
let sum = Number(value1) + value2;
console.log(sum);
An operator is capable of manipulating (mathematical and logical computations) a certain value or operand. There are various operators supported by JavaScript as below,
Arithmetic Operators:
Arithmetic operators are used to perform mathematical operations between numeric operands.
| Operators | Description | Example ( say let x = 10, y = 20;) |
|---|---|---|
| + | Adds two numeric operands. | x + y |
| - | Subtract right operand from left operand | y - x |
| * | Multiply two numeric operands. | x * y |
| / | Divide left operand by right operand. | y / x |
| % | Modulus operator. Returns remainder of two operands. | x % 2 |
| ++ | Increment operator. Increase operand value by one. | x++ |
| -- | Decrement operator. Decrease value by one. | x-- |
Comparison Operators:
JavaScript provides comparison operators that compare two operands and return a boolean value true or false.
| Operators | Description |
|---|---|
| == | Compares the equality of two operands without considering type. |
| === | Compares equality of two operands with type. |
| != | Compares inequality of two operands. |
| > | Returns a boolean value true if the left-side value is greater than the right-side value; otherwise, returns false. |
| < | Returns a boolean value true if the left-side value is less than the right-side value; otherwise, returns false. |
| >= | Returns a boolean value true if the left-side value is greater than or equal to the right-side value; otherwise, returns false. |
| <= | Returns a boolean value true if the left-side value is less than or equal to the right-side value; otherwise, returns false. |
Logical Operators:
The logical operators are used to combine two or more conditions.
1. && - is known as AND operator. It checks whether two operands are non-zero or not (0, false, undefined, null or "" are considered as zero). It returns 1 if they are non-zero; otherwise, returns 0
2. || - is known as OR operator. It checks whether any one of the two operands is non-zero or not (0, false, undefined, null or "" is considered as zero). It returns 1 if any one of of them is non-zero; otherwise, returns 0.
3. ! - is known as NOT operator. It reverses the boolean result of the operand (or condition). !false returns true, and !true returns false.
Assignment Operators:
The assignment operators to assign values to variables with less key strokes.
| Operators | Description |
|---|---|
| = | Assigns right operand value to the left operand. |
| += | Sums up left and right operand values and assigns the result to the left operand. |
| -= | Subtract right operand value from the left operand value and assigns the result to the left operand. |
| *= | Multiply left and right operand values and assigns the result to the left operand. |
| /= | Divide left operand value by right operand value and assign the result to the left operand. |
| %= | Get the modulus of left operand divide by right operand and assign resulted modulus to the left operand. |
Below are the list of bit-wise logical operators used in JavaScript
| Operator | Usage | Description |
|---|---|---|
| Bitwise AND | a & b | Returns a one in each bit position for which the corresponding bits of both operands are ones. |
| Bitwise OR | a | b |
| Bitwise XOR | a ^ b | Returns a one in each bit position for which the corresponding bits of either but not both operands are ones. |
| Bitwise NOT | ~ a | Inverts the bits of its operand. |
| Left shift | a << b | Shifts a in binary representation b (< 32) bits to the left, shifting in zeroes from the right. |
| Sign-propagating right shift | a >> b | Shifts a in binary representation b (< 32) bits to the right, discarding bits shifted off. |
| Zero-fill right shift | a >>> b | Shifts a in binary representation b (< 32) bits to the right, discarding bits shifted off, and shifting in zeroes from the left. |
Examples:
| Operation | Result | Same as | Result |
|---|---|---|---|
| 5 & 1 | 1 | 0101 & 0001 | 0001 |
| `5 | 1` | 5 | 0101 |
~ 5 | -6 | ~0101 | 1010 |
5 << 1 | 10 | 0101 << 1 | 1010 |
5 ^ 1 | 4 | 0101 ^ 0001 | 0100 |
5 >> 1 | 2 | 0101 >> 1 | 0010 |
| 5 >>> 1 | 2 | 0101 >>> 1 | 0010 |
⚝ Try this example on CodeSandbox
== and === operators?JavaScript provides both strict (===, !==) and type-converting (==, !=) equality comparison. The strict operators takes type of variable in consideration, while non-strict operators make type correction/conversion based upon values of variables. The strict operators follow the below conditions for different types,
===, but equal with ==. i.e,
null === undefined --> false but null == undefined --> trueExample:
0 == false // true
0 === false // false
1 == "1" // true
1 === "1" // false
null == undefined // true
null === undefined // false
"0" == false // true
"0" === false // false
[] === [] // false, refer different objects in memory
{} === {} // false, refer different objects in memory
⚝ Try this example on CodeSandbox
In JavaScript, the typeof operator returns the data type of its operand in the form of a string. The operand can be any object, function, or variable.
Example:
console.log(typeof undeclaredVariable); // "undefined"
let a;
console.log(typeof a); // "undefined"
const b = "Hello World";
console.log(typeof b); // "string"
const c = 42;
console.log(typeof c); // "number"
const d = 3.1415;
console.log(typeof d); // "number"
const e = true;
console.log(typeof e); // "boolean"
const f = null;
console.log(typeof f); // "object"
const g = undefined;
console.log(typeof g); // "undefined"
const h = { b: "c" };
console.log(typeof h); // "object"
const i = function () {
return 10;
};
console.log(typeof i); // "function"
⚝ Try this example on CodeSandbox
The unary(+) operator is used to convert a variable to a number. If the variable cannot be converted, it will still become a number but with the value NaN.
Example:
var x = "100";
var y = +x;
console.log(typeof x, typeof y); // string, number
var a = "Hello";
var b = +a;
console.log(typeof a, typeof b, b); // string, number, NaN
⚝ Try this example on CodeSandbox
The delete keyword is used to delete the property as well as its value.
Example:
const user = { name: "Sadhika Chaudhuri", age: 24 };
delete user.age;
console.log(user); // { name: "Sadhika Chaudhuri" }
The conditional (ternary) operator is the only JavaScript operator that takes three operands which acts as a shortcut for if statement.
Syntax:
<condition> ? <value1> : <value2>;
Example:
const isAuthenticated = false;
console.log(isAuthenticated ? 'Hello, welcome' : 'Sorry, you are not authenticated');
Yes, you can apply chaining on conditional operator similar to if … else if … else if … else chain.
Example:
function getValue(someParam) {
return condition1 ? value1
: condition2 ? value2
: condition3 ? value3
: value4;
}
// The above conditional operator is equivalent to:
function getValue(someParam) {
if (condition1) {
return value1;
} else if (condition2) {
return value2;
} else if (condition3) {
return value3;
} else {
return value4;
}
}
typeof and instanceof operator?The typeof operator checks if a value has type of primitive type which can be one of boolean, function, object, number, string, undefined, symbol, or bigint.
Example:
const x = "Hello World";
const y = new String("Hello World");
typeof x; // returns 'string'
typeof y; // returns 'object'
The instanceof is a binary operator, accepting an object and a constructor. It returns a boolean indicating whether or not the object has the given constructor in its prototype chain.
const a = "Hello World";
const b = new String("Hello World");
a instanceof String; // returns false
b instanceof String; // returns true
⚝ Try this example on CodeSandbox
[...'Hello']
Output: ['H', 'e', 'l', 'l', 'o']
Explanation: The string is an iterable type and the spread operator with in an array maps every character of an iterable to one element. Hence, each character of a string becomes an element within an Array.
??) in JavaScript?The nullish coalescing operator (??) returns the right-hand side operand when the left-hand side operand is null or undefined. Otherwise, it returns the left-hand side operand. Unlike ||, it does not treat 0, false, or "" as falsy.
Syntax:
leftExpr ?? rightExpr
Example:
console.log(null ?? 'default'); // 'default'
console.log(undefined ?? 'default'); // 'default'
console.log(0 ?? 'default'); // 0 (0 is NOT null/undefined)
console.log('' ?? 'default'); // '' (empty string is NOT null/undefined)
console.log(false ?? 'default'); // false
// Practical use: provide a fallback for a missing config value
const config = { timeout: 0 };
const timeout = config.timeout ?? 3000;
console.log(timeout); // 0 (not 3000, because 0 is a valid value)
Nullish coalescing assignment (??=):
let user = { name: null };
user.name ??= 'Anonymous';
console.log(user.name); // 'Anonymous'
Logical assignment operators (ES2021) combine a logical operator with assignment. There are three:
| Operator | Equivalent to | Description |
|---|---|---|
&&= | a && (a = b) | Assigns b to a only if a is truthy |
||= | a || (a = b) | Assigns b to a only if a is falsy |
??= | a ?? (a = b) | Assigns b to a only if a is null or undefined |
Example:
// &&= (AND assignment)
let a = 1;
a &&= 2;
console.log(a); // 2 (a was truthy, so assigned)
let b = 0;
b &&= 2;
console.log(b); // 0 (b was falsy, not assigned)
// ||= (OR assignment)
let c = null;
c ||= 'default';
console.log(c); // 'default' (c was falsy)
let d = 'existing';
d ||= 'default';
console.log(d); // 'existing' (d was truthy, not reassigned)
// ??= (Nullish assignment)
let e = null;
e ??= 'fallback';
console.log(e); // 'fallback'
let f = 0;
f ??= 'fallback';
console.log(f); // 0 (0 is not null/undefined)
The Math.random() function returns a floating-point, pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1). For example, if you want generate random integers between 1 to 100, the multiplication factor should be 100,
// Example 01:
Math.random(); // returns a random integer between 0 to 1
// Example 02:
Math.floor(Math.random() * 100) + 1; // returns a random integer from 1 to 100
// Example 03:
function getRandomNumber(max) {
return Math.floor(Math.random() * max) + 1;
}
console.log(getRandomNumber(10)); // returns a random integer from 1 to 10
Random integer between min and max (inclusive):
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 6)); // simulates a dice roll: 1–6
⚝ Try this example on CodeSandbox
The isNaN() function determines whether a value is NaN ( Not a Number ) or not. This function returns true if the value equates to NaN. The isNaN() method converts the value to a number before testing it.
Example:
isNaN('Hello') // true
isNaN('100') // false
typeof NaN // Number
Number.isNaN('Hello'); // false
⚝ Try this example on CodeSandbox
The global isFinite() function determines whether the passed value is a finite number. It returns false if the value is +infinity, -infinity, or NaN (Not-a-Number), otherwise it returns true.
isFinite(Infinity); // false
isFinite(NaN); // false
isFinite(-Infinity); // false
isFinite(100); // true
isFinite(1/0); // false
Number.isFinite(0 / 0); // false
Number.isFinite(null); // false
Number.isFinite("123") // false
⚝ Try this example on CodeSandbox
The Number.NEGATIVE_INFINITY property represents the negative Infinity value.
Syntax:
Number.NEGATIVE_INFINITY
/**
* NEGATIVE_INFINITY
*/
console.log(-10/0); // -Infinity
console.log(Number.NEGATIVE_INFINITY); // -Infinity
console.log(Number.MAX_VALUE + Number.MAX_VALUE); // Infinity
console.log(-2 * Number.MAX_VALUE); // -Infinity
console.log("Math.pow(10, 1000): " + Math.pow(10, 1000)); // Infinity
console.log("Math.log(0): " + Math.log(0)); // -Infinity
console.log(Number.NEGATIVE_INFINITY === -2 * Number.MAX_VALUE); // true
⚝ Try this example on CodeSandbox
1. slice():
The slice() method returns a new array with a copied slice from the original array. The first optional argument is the beginning index and the second optional argument is the ending index (non-inclusive).
Example:
let languages = [ "JavaScript", "Python", "Java", "PHP" ];
languages.slice(1,3); // ["Python", "Java"]
languages.slice(2); // (from index 2 until the end of the array).
// ["Java", "PHP"]
console.log(languages); // the original array is not mutated.
// [ "JavaScript", "Python", "Java", "PHP" ]
2. splice():
The splice() method changes the content of the array in place and can be used to add or remove items from the array.
When only one argument is provided, all the items after the provided starting index are removed from the array.
Example:
let numbers = [10, 20, 30];
numbers.splice(2, 1, 40, 50); // returns removed array:[30]
console.log(numbers); // Original array is mutated.
// returns: [10, 20, 40, 50]
Difference:
| Slice | Splice |
|---|---|
| Doesn't modify the original array(immutable) | Modifies the original array(mutable) |
| Returns the subset of original array | Returns the deleted elements as array |
| Used to pick the elements from array | Used to insert or delete elements to/from array |
⚝ Try this example on CodeSandbox
There are 3 fastest ways to check whether a string contains a substring or not,
1. Using RegEx:
The regular expression test() method checks if a match exists in a string. This method returns true if it finds a match, otherwise, it returns false.
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
let exp1 = /MongoDB/g;
let exp2 = /Ajax/;
exp1.test(str); // true
exp2.test(str); // false
2. Using indexOf:
The indexOf() method is case-sensitive and accepts two parameters. The first parameter is the substring to search for, and the second optional parameter is the index to start the search from (default index is 0).
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
str.indexOf('MongoDB') !== -1 // true
str.indexOf('PHP') !== -1 // false
str.indexOf('Node', 5) !== -1 // true
3. Using includes: (Modern Standard)
The includes() is also case-sensitive and accepts an optional second parameter, an integer which indicates the position where to start searching for.
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
str.includes('MongoDB') // true
str.includes('PHP') // false
str.includes('Node', 5) //true
Use Cases:
| Use Case | Method | Example |
|---|---|---|
| Simple readability check | includes() | Check if a product description contains a keyword |
| Legacy browser support | indexOf() | Check if URL contains a query param |
| Pattern-based search | RegEx test() | Check if input matches a specific format |
// 1. Search bar — check if item name contains user query
const query = "react";
const items = ["React.js Tutorial", "Node.js Guide", "React Native Basics"];
const results = items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
// ["React.js Tutorial", "React Native Basics"]
// 2. URL parsing — check if a query param exists
const url = "https://example.com/search?q=javascript&page=2";
const hasQuery = url.indexOf('?q=') !== -1; // true
// 3. Input validation — check if email contains '@' and '.'
const email = "user@example.com";
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); // true
⚝ Try this example on CodeSandbox
The trim() method removes whitespace from both sides of a string. JavaScript provides 3 simple functions on how to trim strings.
1. string.trim():
The string.trim() removes sequences of whitespaces and line terminators from both the start and the end of the string.
const name = " Karan Talwar ";
console.log(name.trim()); // => 'Karan Talwar'
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trim()); // => '80-555-123'
2. string.trimStart():
The string.trimStart() removes sequences of whitespaces and line terminators only from the start of the string.
const name = " Karan Talwar ";
console.log(name.trimStart()); // => "Karan Talwar "
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trimStart()); // => "80-555-123 "
3. string.trimEnd():
The string.trimEnd() removes sequences of whitespaces and line terminators only from the end of the string.
const name = " Karan Talwar ";
console.log(name.trimEnd()); // => " Karan Talwar"
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trimEnd()); // => " 80-555-123"
⚝ Try this example on CodeSandbox
The eval() function evaluates JavaScript code represented as a string. The string can be a JavaScript expression, variable, statement, or sequence of statements.
console.log(eval('10 + 20')); // 30
let x = 10;
let y = 20;
let z = '50';
eval('x + y + 1'); // returns 31
eval(z); // returns 50
If the argument of eval() is not a string, eval() returns the argument unchanged. In the following example, the String constructor is specified and eval() returns a String object rather than evaluating the string.
eval(new String('10 + 20')); // returns a String object containing "10 + 20"
eval('10 + 20'); // returns 30
// work around
let expression = new String('10 + 20');
eval(expression.toString()); // returns 30
Warning: Executing JavaScript from a string is an enormous security risk. It is far too easy for a bad actor to run arbitrary code when you use eval().
⚝ Try this example on CodeSandbox
You can use String.prototype.startsWith() method to check if a string starts with another string or not. It is fully supported in all modern browsers.
let str = "Hello World";
console.log(str.startsWith("Hello")); // true
console.log(str.startsWith("World")); // false
⚝ Try this example on CodeSandbox
replaceAll(), padStart() and padEnd() string methods?1. String.prototype.replaceAll():
The replaceAll() method returns a new string with all matches of a pattern replaced by a replacement. Unlike replace(), it replaces every occurrence without needing a global regex flag.
const text = 'I like cats. Cats are cute cats.';
console.log(text.replace('cats', 'dogs')); // 'I like dogs. Cats are cute cats.' (only first)
console.log(text.replaceAll('cats', 'dogs')); // 'I like dogs. Cats are cute dogs.'
2. String.prototype.padStart():
The padStart() method pads the current string with another string from the start until it reaches the given length.
console.log('5'.padStart(3, '0')); // '005'
console.log('hello'.padStart(8)); // ' hello' (default pad is space)
console.log('42'.padStart(5, '*')); // '***42'
3. String.prototype.padEnd():
The padEnd() method pads the current string with another string from the end until it reaches the given length.
console.log('5'.padEnd(3, '0')); // '500'
console.log('hello'.padEnd(8)); // 'hello '
console.log('42'.padEnd(5, '-')); // '42---'
JavaScript array is an object that represents a collection of similar type of elements. It can holds values (of any type) not particularly in named properties/keys, but rather in numerically indexed positions.
Syntax:
const array_name = [item-1, item-2, item-3, ...];
Example 01: Creating an array
// array of numbers
const numbers = [10, 20, 30, 40, 50];
// using new keyword
const numbers = new Array(10, 20, 30, 40, 50);
// array of strings
let fruits = ["Apple", "Orange", "Plum", "Mango"];
// array of character
const numbers = Array.from("abc"); // ["a", "b", "c"]
// array of numbers
const numbers = Array.of(10, 20, 30); // [10, 20, 30]
Common Methods
| Category | Methods |
|---|---|
| Add/Remove | push, pop, shift, unshift, splice |
| Extract | slice, flat, flatMap |
| Search | find, findIndex, indexOf, includes |
| Iterate | forEach, map, filter, reduce |
| Sort/Order | sort, reverse |
| Combine | concat, join |
Example 02: Accessing array elements
const numbers = [10, 20, 30, 40, 50];
// Transforming
console.log(numbers.map(n => n * 2)); // [20, 40, 60, 80, 100]
// Filtering
console.log(numbers.filter(n => n % 2 === 0)); // [10, 20, 30, 40, 50]
// Reducing
console.log(numbers.reduce((sum, n) => sum + n, 0)); // 150
// Searching
console.log(numbers.find(n => n > 30)); // 40
console.log(numbers.includes(30)); // true
Example 03: Adding new array elements
let fruits = ["Apple", "Orange", "Plum", "Mango"];
fruits.push("Grapes"); // Adds a new element (Grapes) to fruits
⚝ Try this example on CodeSandbox
JavaScript does not have built-in associative arrays in the way languages like PHP or literal dictionary structures do. Instead, JavaScript uses regular Objects or Maps to handle key-value pairs (associative behavior).
Associative arrays are basically objects in JavaScript where indexes are replaced by user-defined keys. They do not have a length property like a normal array and cannot be traversed using a normal for loop.
Syntax:
const array_name = { key1: 'value1', key2: 'value2', key3: 'value3' }
Example: Plain Object (most common)
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
// Accessing employee elements
console.log(employee.id); // 12345
console.log(employee.name); // Sakshi Memon
// Array Length
console.log(Object.keys(employee).length); // 3
// Retrieve the elements
for (let key in employee) {
console.log(key + " = " + employee[key]);
}
// Output
id = 12345
name = Sakshi Memon
email = sakshi.memon@email.com
Example: Map (true associative structure)
const map = new Map();
map.set("name", "Alice");
map.set(42, "answer"); // keys can be any type
map.set({ id: 1 }, "object key");
map.get("name"); // "Alice"
map.has(42); // true
map.size; // 3
⚝ Try this example on CodeSandbox
Method 1: Using Object.keys().length
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
console.log(Object.keys(employee).length); // 3
Method 2: Using Object.hasOwnProperty()
function getLength(object) {
let count = 0;
for (let key in object) {
// hasOwnProperty method check own property of object
if (object.hasOwnProperty(key)) count++;
}
return count;
}
console.log(getLength(employee)); // 3
Method 3: Using Object.getOwnPropertyNames()
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
Object.getOwnPropertyNames(employee).length; // 3
⚝ Try this example on CodeSandbox
Objects represent a special data type that is mutable and can be used to store a collection of data (rather than just a single value). Arrays are a special type of variable that is also mutable and can also be used to store a list of values.
Example: Arrays
const numbers = [10, 20, 30];
// Iterating through loop
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
// Pop an element from array
numbers.pop();
console.log("after pop(): " + numbers);
Example: Array of Objects
const employees = [
{ id: 101, name: "Sakshi Memon", email: "sakshi.memon@email.com" },
{ id: 102, name: "Subhash Shukla", email: "subhash.shukla@email.com" },
{ id: 103, name: "Mohini Karpe", email: "mohini.karpe@email.com" }
];
// Using DOT notation
console.log(employees[0].name);
// Using delete keyword
delete employees[0];
// Iterating using for..in loop
for (let key in employees) {
console.log(employees[key]);
}
Difference:
| S.No. | Array | Array of objects |
|---|---|---|
| 1. | Arrays are best to use when the elements are numbers. | Objects are best to use when the elements strings |
| 3. | The elements can be manipulated using []. | The properties can be manipulated using both . ( DOT ) notation and []. |
| 4. | The elements can be popped out of an array using the pop() function. | The keys or properties can be deleted by using the delete keyword. |
| 5. | Iterating through an array is possible using For loop, For..in, For..of, and ForEach(). | Iterating through an array of objects is possible using For..in, For..of, and ForEach(). |
⚝ Try this example on CodeSandbox
1. array.join():
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
var elements = ['Fire', 'Air', 'Water'];
console.log(elements.join()); // Output: "Fire,Air,Water"
console.log(elements.join('')); // Output: "FireAirWater"
console.log(elements.join('-')); // Output: "Fire-Air-Water"
2. array.pop():
The pop() method removes the last element from an array and returns that element. This method changes the length of the array.
var plants = ['broccoli', 'cauliflower', 'kale'];
console.log(plants.pop()); // Output: "kale"
console.log(plants); // Output: Array ["broccoli", "cauliflower"]
console.log(plants.pop()); // Output: "cauliflower"
console.log(plants.pop()); // Output: "broccoli"
console.log(plants.pop()); // Output: "undefined"
3. array.push():
The push() method adds one or more elements to the end of an array and returns the new length of the array.
const animals = ['pigs', 'goats', 'sheep'];
const count = animals.push('cows');
console.log(count); // Output: 4
console.log(animals); // Output: Array ["pigs", "goats", "sheep", "cows"]
4. array.shift():
The shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.shift();
console.log(fruits) // Output: Array ["Orange", "Apple", "Mango"]
5. array.unshift():
The unshift() method adds one or more elements to the beginning of an array and returns the new length of the array.
var fruits = ["Banana", "Orange", "Apple"];
fruits.unshift("Mango","Pineapple");
console.log(fruits); // Output: Array ["Mango", "Pineapple", "Banana", "Orange", "Apple"]
6. array.concat():
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
console.log(array1.concat(array2)); // Output: Array ["a", "b", "c", "d", "e", "f"]
7. array.map():
The map() method creates a new array with the results of calling a provided function on every element in the calling array.
var array1 = [1, 4, 9, 16];
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1); // Output: Array [2, 8, 18, 32]
8. array.filter():
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
var words = ['spray', 'limit', 'elite', 'exuberant', 'destruction'];
const result = words.filter(word => word.length > 6);
console.log(result); // Output: Array ["exuberant", "destruction"]
9. array.reduce():
The reduce() method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;
console.log(array1.reduce(reducer)); // Output: 10
console.log(array1.reduce(reducer, 5)); // Output: 15
10. array.reduceRight():
The reduceRight() method applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.
const array1 = [[0, 1], [2, 3], [4, 5]].reduceRight(
(accumulator, currentValue) => accumulator.concat(currentValue)
);
console.log(array1); // Output: Array [4, 5, 2, 3, 0, 1]
11. array.every():
The every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.
function isBelowThreshold(currentValue) {
return currentValue < 40;
}
var array1 = [1, 30, 39, 29, 10, 13];
console.log(array1.every(isBelowThreshold)); // Output: true
12. array.some():
The some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value.
var array = [1, 2, 3, 4, 5];
var even = function(element) {
// checks whether an element is even
return element % 2 === 0;
};
console.log(array.some(even)); // Output: true
13. array.indexOf():
The indexOf() method returns the first index at which a given element can be found in the array, or -1 if it is not present.
var beasts = ['ant', 'bison', 'camel'];
console.log(beasts.indexOf('camel')); // Output: 2
console.log(beasts.indexOf('giraffe')); // Output: -1
14. array.lastIndexOf():
The lastIndexOf() method returns the index within the calling String object of the last occurrence of the specified value, searching backwards from fromIndex. Returns -1 if the value is not found.
var paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?';
var searchTerm = 'dog';
console.log('The index of the first "' + searchTerm + '" from the end is ' + paragraph.lastIndexOf(searchTerm));
// Output: "The index of the first "dog" from the end is 52"
15. array.find():
The find() method returns the value of the first element in the provided array that satisfies the provided testing function.
var array1 = [5, 12, 8, 130, 44];
var found = array1.find(function(element) {
return element > 100;
});
console.log(found); // Output: 130
16. array.findIndex():
The findIndex() method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test.
var array1 = [5, 12, 8, 130, 44];
function isLargeNumber(element) {
return element > 20;
}
console.log(array1.findIndex(isLargeNumber)); // Output: 3
17. array.includes():
The includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate.
var array1 = [1, 2, 3];
console.log(array1.includes(2)); // Output: true
var pets = ['cat', 'dog', 'bat'];
console.log(pets.includes('at')); // Output: false
18. array.at():
The at() method (ES2022) takes an integer and returns the item at that index. Negative integers count back from the last item.
const fruits = ['Apple', 'Orange', 'Banana', 'Mango'];
console.log(fruits.at(0)); // Output: Apple
console.log(fruits.at(-1)); // Output: Mango (last element)
console.log(fruits.at(-2)); // Output: Banana
19. array.flat():
The flat() method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // Output: [1, 2, 3, 4, [5, 6]]
console.log(nested.flat(2)); // Output: [1, 2, 3, 4, 5, 6]
console.log(nested.flat(Infinity)); // Output: [1, 2, 3, 4, 5, 6]
20. array.flatMap():
The flatMap() method maps each element using a mapping function, then flattens the result into a new array (one level deep).
const sentences = ['Hello World', 'Foo Bar'];
console.log(sentences.flatMap(s => s.split(' '))); // Output: ['Hello', 'World', 'Foo', 'Bar']
21. array.findLast():
The findLast() method (ES2023) iterates the array in reverse and returns the value of the first element that satisfies the provided testing function.
const numbers = [5, 12, 8, 130, 44];
console.log(numbers.findLast(n => n > 10)); // Output: 44
22. array.findLastIndex():
The findLastIndex() method (ES2023) iterates the array in reverse and returns the index of the first element that satisfies the provided testing function.
const numbers = [5, 12, 8, 130, 44];
console.log(numbers.findLastIndex(n => n > 10)); // Output: 4
Spread operator or Spread Syntax allow us to expand the arrays and objects into elements in the case of an array and key-value pairs in the case of an object.
Example:
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers));
// ES-5 way
console.log(sum.apply(null, numbers));
Example: Merge arrays
const newBrands = ["Tesla", "Mahindra"];
const brands = ["Ford", "Honda", ...newBrands, "BMW"];
console.log(brands);
Example: Copy array/object
let obj = { a: 10, b: 20, c: 30 };
// spread the object into a list of parameters
let objCopy = { ...obj };
// add new
obj.d = 40;
console.log(JSON.stringify(obj)); // { "a":10, "b":20, "c":30, "d":40 }
console.log(JSON.stringify(objCopy)); // { "a":10, "b":20, "c":30 }
Difference:
The main difference between rest and spread is that the rest operator puts the rest of some specific user-supplied values into a JavaScript array. But the spread syntax expands iterables into individual elements.
| Spread Syntax | Rest Syntax |
|---|---|
| Spread operator as its name suggests it spreads or expands the content of the given element. | Rest Syntax is just the opposite of spread syntax it collects the data and stores that data in a variable which we can use further in our code. |
| It expands an Array in form of elements, while in key-value pairs in the case of Objects. | It collects the data in the developer's desired format. |
| You may or may not use the strict mode inside the function containing the spread operator. | You can not use the strict mode inside function containing the rest operator. |
| It will overwrite the identical properties inside two objects and replace the former with the latter. | It simply collects all properties and wraps them inside a container. |
⚝ Try this example on CodeSandbox
Both for..of and for..in statements iterate over lists; the values iterated on are different though, for..in returns a list of keys on the object being iterated, whereas for..of returns a list of values of the numeric properties of the object being iterated.
String, Array, array-like objects (e.g., arguments or NodeList), TypedArray, Map, Set, and user-defined iterables.Example:
// for..in
const list = [10, 20, 30];
for (let key in list) {
console.log(key); // "0", "1", "2",
}
// for..of
for (let value of list) {
console.log(value); // "10", "20", "30"
}
// Works on any iterable: arrays, strings, Map, Set
for (let char of "hello") {
console.log(char); // "h", "e", "l", "l", "o"
}
⚝ Try this example on CodeSandbox
1. forEach():
It takes a callback function and run that callback function on each element of array one by one. Basically forEach works as a traditional for loop looping over the array and providing array elements to do operations on them.
let numbers = [10, 20, 30];
numbers.forEach(function (number, index) {
console.log(number + " comes at " + index);
});
// Output
10 comes at 0
20 comes at 1
30 comes at 2
2. filter():
The main difference between forEach() and filter() is that forEach just loop over the array and executes the callback but filter executes the callback and check its return value. If the value is true element remains in the resulting array but if the return value is false the element will be removed for the resulting array.
Note: filter does not update the existing array it will return a new filtered array every time.
let numbers = [10, 20, 30];
let result = numbers.filter(function (number) {
return number !== 20;
});
console.log(result);
// Output
[10, 30]
3. map():
map() like filter() & forEach() takes a callback and run it against every element on the array but whats makes it unique is it generate a new array based on your existing array.
Like filter(), map() also returns an array. The provided callback to map modifies the array elements and save them into the new array upon completion that array get returned as the mapped array.
let numbers = [10, 20, 30];
let mapped = numbers.map(function (number) {
return number * 10;
});
console.log(mapped);
// Output
[100, 200, 300]
4. reduce():
reduce() method of the array object is used to reduce the array to one single value.
let numbers = [10, 20, 30];
let sum = numbers.reduce(function (sum, number) {
return sum + number;
});
console.log(sum); // Output: 60
Key Rules
map()filter()reduce()forEach()forEach() when you need a result — use map()/filter()/reduce() instead⚝ Try this example on CodeSandbox
JSON is an acronym for JavaScript Object Notation, and is "an open standard data interchange format".
JSON array represents ordered list of values. JSON array can store multiple values. It can store string, number, boolean or object in JSON array.
// Empty JSON array
const empty = [ ];
// JSON Array of Numbers
const numbers = [12, 34, 56, 43, 95];
// JSON Array of Objects
{
"employees": [
{ "name": "Kabir Dixit", "email": "kabir.dixit@gmail.com", "age": 23 },
{ "name": "Mukta Bhagat", "email": "mukta.bhagat@gmail.com", "age": 28 },
{ "name": "Sakshi Ramakrishnan", "email": "sakshi.ramakrishnan@gmail.com", "age": 33 }
]
}
// access array values
console.log(employees[0].name) // Kabir Dixit
JSON.parse() function will use string and converts to JSON object and if it parses invalidate JSON data, it throws an exception ( Uncaught SyntaxError: Unexpected string in JSON ).
function isValidJson(json) {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
}
console.log(isValidJson("{}")); // true
console.log(isValidJson("abc")); // false
⚝ Try this example on CodeSandbox
When sending data to a web server, the data has to be in a string format. The JSON.stringify() method converts a JavaScript object or value to a JSON string format.
const user = {'name': 'Shashi Meda', 'email': 'shashi.meda@email.com', 'age': 28}
console.log(JSON.stringify(user)); // {"name":"Shashi Meda","email":"shashi.meda@email.com","age":28}
When receiving the data from a web server, the data is always in a string format. But you can convert this string value to javascript object using JSON.parse() method.
const user = '{"name": "Shashi Meda", "email": "shashi.meda@email.com", "age": 28}'
console.log(JSON.parse(user));// {'name': 'Shashi Meda', 'email': 'shashi.meda@email.com', 'age': 28}
The purpose of the compare function is to define an alternative sort order. When the sort() function compares two values, it sends the values to the compare function, and sorts the values according to the returned (negative, zero, positive) value.
If omitted, the array elements are converted to strings, then sorted according to each character's Unicode code point value.
const numbers = [1, 2, 5, 3, 4];
numbers.sort((a, b) => b - a);
console.log(numbers); // [5, 4, 3, 2, 1]
⚝ Try this example on CodeSandbox
.forEach loop and a .map() loop and why you would pick one versus the other?To understand the differences between the two, Let us look at what each function does.
1. Array.forEach():
const numbers = [10, 20, 30];
const doubled = numbers.forEach((num, index) => {
return num * 2;
});
console.log(doubled) // undefined
2. Array.map():
const numbers = [10, 20, 30];
const doubled = numbers.map(num => {
return num * 2;
});
console.log(doubled) // [20, 40, 60]
The main difference between .forEach and .map() is that .map() returns a new array. If you need the result, but do not wish to mutate the original array, .map() is the clear choice. If you simply need to iterate over an array, forEach is a fine choice.
⚝ Try this example on CodeSandbox
The unshift() method adds one or more elements to the beginning of an array and returns the new length of the array.
Example:
const numbers = [10, 20, 30];
console.log(numbers.unshift(40, 50)); // 5
console.log(numbers); // [40, 50, 10, 20, 30]
⚝ Try this example on CodeSandbox
The rest parameter is used to represent an indefinite number of arguments as an array. The important point here is only the function's last parameter can be a "rest parameter".
This feature has been introduced to reduce the boilerplate code that was induced by the arguments.
Example:
function sum(...args) {
return args.reduce((previous, current) => {
return previous + current;
});
}
console.log(sum(10)); // 10
console.log(sum(10, 20)); // 30
console.log(sum(10, 20, 30)); // 60
⚝ Try this example on CodeSandbox
The rest parameter should be the last argument, as its job is to collect all the remaining arguments into an array.
Example: If you define a function like below it does not make any sense and will throw an SyntaxError.
function display(a, ...args, b) {
console.log(a);
for (let i = 0; i < args.length; i++) {
console.log(args[i]);
}
console.log(b);
}
display(10, 20, 30, 40, 50);
// Output
SyntaxError: Rest element must be last element
⚝ Try this example on CodeSandbox
[] and new Array() are two different ways of creating an array, but they are functionally equivalent.
The primary difference between them is in how they are created and in their behavior when used with certain methods.
[] is a shorthand for creating a new array. It is the preferred way to create an array in most cases, because it's more concise and easier to read. For example:
const myArray = []; // create a new empty array
On the other hand, new Array() is a constructor function that creates a new array object. It can be used to create an array of a specific length or with specific elements. For example:
const myArray = new Array(); // create a new empty array
const myOtherArray = new Array(3); // create a new array with a length of 3
const myThirdArray = new Array("a", "b", "c"); // create a new array with three elements
One potential pitfall of using new Array() is that it can be ambiguous when you pass a single argument to the constructor. For example, new Array(3) creates an array with a length of 3, but new Array("3") creates an array with a single element, the string "3". This is because the argument is treated as the value of the first element when it's a non-negative integer, but as the length of the array when it's a string or a negative integer.
In summary, [] is the preferred way to create a new array in JavaScript, while new Array() is an alternative way that can be used when you need more control over the array's length or contents.
Array.from(), Array.of() and Array.isArray() methods?1. Array.from():
Array.from() creates a new, shallow-copied array from an array-like or iterable object such as a Set, Map, NodeList, or string.
// From a string
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']
// From a Set
const set = new Set([1, 2, 3, 2, 1]);
console.log(Array.from(set)); // [1, 2, 3]
// With a map function
console.log(Array.from([1, 2, 3], x => x * 2)); // [2, 4, 6]
// From array-like object
console.log(Array.from({ length: 3 }, (_, i) => i + 1)); // [1, 2, 3]
2. Array.of():
Array.of() creates a new array from a variable number of arguments, regardless of number or type. It fixes the ambiguity of new Array() with a single numeric argument.
console.log(Array.of(7)); // [7] (one element)
console.log(new Array(7)); // [ , , , , , , ] (empty array of length 7!)
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
3. Array.isArray():
Array.isArray() returns true if the passed value is an array, false otherwise. It is more reliable than instanceof when working across different frames or windows.
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray('hello')); // false
console.log(Array.isArray({ length: 3 })); // false
console.log(Array.isArray(new Array())); // true
Control flow determines the order in which statements are executed. JavaScript provides three main categories:
| Category | Statements |
|---|---|
| Conditional | if, else if, else, switch, ternary (? :) |
| Looping / Iteration | for, while, do...while, for...in, for...of |
| Jump / Transfer | break, continue, return, throw |
By default, JavaScript executes code top-to-bottom (sequential flow). Control flow statements allow branching and repetition, enabling complex logic.
Example:
// Conditional branching
const score = 78;
if (score >= 90) {
console.log("Grade: A");
} else if (score >= 75) {
console.log("Grade: B"); // ← executed
} else {
console.log("Grade: C");
}
// Looping
const students = ["Alice", "Bob", "Carol"];
for (let i = 0; i < students.length; i++) {
console.log(students[i]);
}
// Jump: exit loop early
for (let i = 0; i < 10; i++) {
if (i === 5) break;
console.log(i); // 0, 1, 2, 3, 4
}
if...else if...else ladder work, and what are its performance considerations?The if...else if...else ladder evaluates conditions sequentially from top to bottom and executes the first block whose condition is true. Once a matching branch executes, all remaining else if and else blocks are skipped — making it short-circuit by design.
Performance consideration: JavaScript evaluates each condition until one is truthy. Placing the most frequently matched condition first reduces the number of comparisons at runtime.
Example:
function classify(temperature) {
if (temperature < 0) {
return "Freezing";
} else if (temperature < 10) {
return "Cold";
} else if (temperature < 20) {
return "Mild";
} else if (temperature < 30) {
return "Warm";
} else {
return "Hot";
}
}
console.log(classify(-5)); // Freezing
console.log(classify(15)); // Mild
console.log(classify(35)); // Hot
Falsy vs. Truthy awareness:
// These values are falsy in JavaScript:
// false, 0, "", null, undefined, NaN
const value = 0;
if (value) {
console.log("Truthy");
} else {
console.log("Falsy"); // ← executed because 0 is falsy
}
switch statement instead of if...else if, and what are its pitfalls?A switch statement compares a single expression against multiple discrete values using strict equality (===). It is preferable over if...else if when:
Pitfall — Fall-through: Without a break statement, execution "falls through" to the next case, which is a common source of bugs.
Example:
const day = "Monday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("Weekday");
break;
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Unknown day");
}
// Output: Weekday
Intentional fall-through example:
// Grouping cases that share behavior — intentional fall-through
const statusCode = 404;
switch (statusCode) {
case 400:
case 401:
case 403:
case 404:
console.log("Client error");
break;
case 500:
case 502:
case 503:
console.log("Server error");
break;
default:
console.log("Unknown status");
}
// Output: Client error
Pitfall — switch uses ===, not ==:
const x = "1";
switch (x) {
case 1:
console.log("Number 1"); // NOT matched
break;
case "1":
console.log("String '1'"); // ← matched
break;
}
for, for...in, and for...of loops with practical examples.| Loop | Iterates Over | Use Case |
|---|---|---|
for | Index-based counter | Arrays, precise iteration control |
for...in | Enumerable property keys | Object properties (avoid for arrays) |
for...of | Iterable values (arrays, strings, Maps, Sets, generators) | Arrays, strings, and other iterables |
Example:
// --- for loop: classic index-based ---
const arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
console.log(`Index ${i}: ${arr[i]}`);
}
// Index 0: 10 | Index 1: 20 | Index 2: 30
// --- for...in: object property keys ---
const user = { name: "Alice", age: 30, role: "admin" };
for (const key in user) {
console.log(`${key}: ${user[key]}`);
}
// name: Alice | age: 30 | role: admin
// --- for...of: iterable values ---
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
// apple | banana | cherry
// for...of also works on strings
for (const char of "hello") {
process.stdout.write(char + " ");
}
// h e l l o
// for...of with Map
const scores = new Map([["Alice", 95], ["Bob", 87]]);
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
Why avoid for...in on arrays?
const arr = [1, 2, 3];
arr.custom = "oops"; // accidentally adding a property
for (const key in arr) {
console.log(key); // "0", "1", "2", "custom" ← includes inherited/custom keys!
}
for (const val of arr) {
console.log(val); // 1, 2, 3 ← only values, safe
}
Real-World Use Case:
In a Node.js data-processing pipeline, for...of with await (inside an async function) iterates over a paginated API result set sequentially — something forEach cannot do since it ignores returned Promises. for...in is used to dynamically enumerate config object keys when serializing environment-specific settings.
while and do...while loops differ, and when should each be used?| Loop | Condition Check | Minimum Executions | Use When |
|---|---|---|---|
while | Before each iteration | 0 (may never run) | Number of iterations is unknown; condition may be false initially |
do...while | After each iteration | 1 (always runs once) | Body must execute at least once regardless of condition |
Example:
// --- while loop ---
let stock = 5;
while (stock > 0) {
console.log(`Items remaining: ${stock}`);
stock--;
}
// Runs 5 times. If stock were 0 initially, it would never run.
// --- do...while loop ---
let attempts = 0;
let pin;
do {
pin = prompt("Enter 4-digit PIN:"); // Must show prompt at least once
attempts++;
} while (pin !== "1234" && attempts < 3);
console.log(attempts === 3 ? "Locked out" : "Access granted");
Infinite loop guard — always ensure the condition can become false:
// DANGER: this never terminates
// while (true) { }
// SAFE: controlled exit with break
let counter = 0;
while (true) {
counter++;
if (counter >= 5) break;
}
console.log(counter); // 5
break and continue statements, and how do they interact with nested loops?break: Immediately terminates the innermost loop or switch statement and transfers control to the statement following it.continue: Skips the current iteration of the innermost loop and jumps to the next iteration's condition check.Example:
// --- break: stop on first match ---
const inventory = [
{ id: 1, name: "Widget A", inStock: false },
{ id: 2, name: "Widget B", inStock: true },
{ id: 3, name: "Widget C", inStock: true },
];
let firstAvailable = null;
for (const item of inventory) {
if (item.inStock) {
firstAvailable = item;
break; // stop searching once found
}
}
console.log(firstAvailable); // { id: 2, name: "Widget B", inStock: true }
// --- continue: skip unwanted items ---
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = [];
for (const n of numbers) {
if (n % 2 !== 0) continue; // skip odd numbers
evens.push(n);
}
console.log(evens); // [2, 4, 6, 8, 10]
Labeled statements — breaking outer loops:
// Without labels, break only exits the inner loop
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outerLoop; // exits BOTH loops
}
console.log(`i=${i}, j=${j}`);
}
}
// i=0,j=0 | i=0,j=1 | i=0,j=2 | i=1,j=0 ← stops here
Real-World Use Case:
In a search autocomplete engine, break short-circuits a loop over a large dataset once the desired number of suggestions is collected, preventing unnecessary iterations. continue is used in data-cleaning pipelines to skip null or malformed records without nesting the entire logic in an extra if block — keeping cyclomatic complexity low and the code easier to review.
if...else for conditional expressions?The ternary operator (condition ? expressionIfTrue : expressionIfFalse) is JavaScript's only three-operand operator. It evaluates a condition and returns one of two expressions. Unlike if...else (a statement), the ternary is an expression — it produces a value and can be used inline.
Example:
// if...else statement — cannot be used inline
let label;
const isLoggedIn = true;
if (isLoggedIn) {
label = "Logout";
} else {
label = "Login";
}
// Ternary expression — equivalent, concise, assignable
const label2 = isLoggedIn ? "Logout" : "Login";
console.log(label2); // "Logout"
// Inline in JSX (React)
// <button>{isLoggedIn ? "Logout" : "Login"}</button>
// Nested ternary — use sparingly, prefer if...else for readability
const score = 72;
const grade = score >= 90 ? "A"
: score >= 75 ? "B"
: score >= 60 ? "C"
: "F";
console.log(grade); // "C"
When NOT to use ternary:
// Bad: using ternary for side effects reduces readability
isLoggedIn ? doLogout() : doLogin();
// Better: use if...else for side effects
if (isLoggedIn) {
doLogout();
} else {
doLogin();
}
Real-World Use Case:
In React, the ternary operator is the standard pattern for conditional rendering within JSX since JSX is an expression context — if statements cannot appear inside {} interpolation. For example, rendering a loading spinner vs. a data table based on isLoading state is universally handled with a ternary.
??) and optional chaining (?.) operators, and how do they relate to control flow?Both operators introduced in ES2020 reduce verbose defensive if checks for null/undefined values:
??): Returns the right-hand operand when the left is null or undefined (not other falsy values like 0 or "").?.): Short-circuits and returns undefined instead of throwing a TypeError when accessing a property on null or undefined.Example:
// --- Nullish Coalescing ?? ---
const userAge = 0; // falsy but valid!
// Old approach (bug: treats 0 as falsy)
const age1 = userAge || 18; // 18 ← WRONG: overwrites valid 0
// Correct approach
const age2 = userAge ?? 18; // 0 ← CORRECT: 0 is not null/undefined
console.log(age1, age2); // 18, 0
// --- Optional Chaining ?. ---
const user = {
profile: {
address: null
}
};
// Without optional chaining — throws TypeError
// console.log(user.profile.address.city); // TypeError!
// With optional chaining — safe
console.log(user.profile?.address?.city); // undefined (no error)
// Works on methods too
const result = user.getPermissions?.(); // undefined if method doesn\'t exist
// Works on arrays
const first = user.tags?.[0]; // undefined if tags is null/undefined
// --- Combined usage ---
const city = user.profile?.address?.city ?? "Unknown City";
console.log(city); // "Unknown City"
Real-World Use Case:
When consuming third-party REST APIs or GraphQL responses, deeply nested fields may be absent depending on the query. Optional chaining eliminates dozens of guard clauses like if (response && response.data && response.data.user). Nullish coalescing is critical when a user-configurable setting has 0 or "" as a valid value — falling back with || would incorrectly override those legitimate settings.
A labeled statement is an identifier followed by a colon (:) placed before a loop or block. Labels can be referenced by break and continue to control the flow of outer loops from within a nested loop. They are rarely used — but in specific nested-iteration scenarios they can be clearer than managing manual flags.
Example:
// --- Using label with break to exit a matrix search ---
function findValue(matrix, target) {
let found = false;
search: for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] === target) {
console.log(`Found at [${row}][${col}]`);
found = true;
break search; // exit both loops immediately
}
}
}
return found;
}
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
findValue(matrix, 5); // Found at [1][1]
// --- Using label with continue ---
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) continue outer; // skip rest of inner loop, go to next outer iteration
console.log(`i=${i}, j=${j}`);
}
}
// i=0,j=0 | i=1,j=0 | i=2,j=0
Alternative to a flag variable:
// Without labels — requires a flag
let found = false;
for (let i = 0; i < rows && !found; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === target) { found = true; break; }
}
}
// With labels — no flag needed, intent is clearer
searchGrid: for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === target) break searchGrid;
}
}
Real-World Use Case:
In a pathfinding algorithm (e.g., BFS/DFS on a 2D game map) or a seat-selection engine scanning rows and seats in a stadium booking system, a labeled break exits all nested loops the moment a valid seat is located — avoiding a boolean flag variable that would otherwise pollute the outer scope and add an extra condition to every loop header.
JavaScript's logical operators (&&, ||, ??) use short-circuit evaluation: the right-hand operand is only evaluated if necessary. This is not just a performance optimization — it is a widely used control flow mechanism.
| Operator | Short-circuits when left side is | Returns |
|---|---|---|
&& (AND) | falsy | Left operand (skips right) |
|| (OR) | truthy | Left operand (skips right) |
?? (Nullish) | not null/undefined | Left operand (skips right) |
Example:
// --- && short-circuit: right side only runs if left is truthy ---
const user = { isAdmin: true };
user.isAdmin && console.log("Admin panel loaded"); // ← runs
null && console.log("This never runs"); // ← skipped
// --- || short-circuit: right side only runs if left is falsy ---
const config = null;
const settings = config || { theme: "dark", lang: "en" }; // default fallback
console.log(settings); // { theme: "dark", lang: "en" }
// --- Practical: guard before calling a function ---
function logError(err) {
err && err.message && console.error(err.message);
}
logError(null); // nothing happens (safe)
logError({ message: "Failed" }); // "Failed"
// --- Chaining for conditional execution (React pattern) ---
const isLoading = false;
const data = [1, 2, 3];
// Equivalent to: if (!isLoading && data.length > 0) render()
const output = !isLoading && data.length > 0 && "Render data";
console.log(output); // "Render data"
Gotcha — using && for side effects:
// This is valid but can be confusing:
isAuthenticated && fetchDashboard();
// Prefer explicit if for clarity in complex flows:
if (isAuthenticated) {
fetchDashboard();
}
Real-World Use Case:
In React, && short-circuit is the idiomatic pattern for conditional rendering: {isLoggedIn && <UserMenu />} renders the component only when isLoggedIn is truthy, with no JSX if block needed. The || operator is universally used for default parameter fallbacks before optional chaining became standard. In Node.js middleware chains, && guards ensure a dependency (e.g., a database connection) is available before proceeding to query logic.
Arrows is a new syntax for functions, which brings several benefits:
this to the surrounding code's context=> is shorter and simpler than function, although stylistic issues are often subjectiveExample 01: Arrow Function with No Argument
If a function doesn't take any argument, then you should use empty parentheses.
let greet = () => console.log('Hello');
greet(); // Hello
Example 02: Arrow Function with One Argument
If a function has only one argument, you can omit the parentheses.
let greet = x => console.log(x);
greet('Hello'); // Hello
Example 03: Arrow Function as an Expression
You can also dynamically create a function and use it as an expression.
let age = 25;
let welcome = (age < 18) ?
() => console.log('Baby') :
() => console.log('Adult');
welcome(); // Adult
Example 04: Multiline Arrow Functions
If a function body has multiple statements, you need to put them inside curly brackets {}.
let area = (r) => {
const pi = 3.14;
return pi * r * r;
}
let result = area(10);
console.log(result); // 314
Note: Unlike regular functions, arrow functions do not have their own this. The value of this inside an arrow function remains the same throughout the lifecycle of the function and is always bound to the value of this in the closest non-arrow parent function.
⚝ Try this example on CodeSandbox
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. So, when the constructor is used to create a new object, this will always refer to that object.
Example:
const Person = function(firstName) {
this.firstName = firstName;
this.sayName1 = function() { console.log(this.firstName); }; // regular function
this.sayName2 = () => { console.log(this.firstName); }; // arrow function
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// Regular function: 'this' can be changed with call/apply/bind
john.sayName1.call(dave); // Dave
john.sayName2.call(dave); // John — arrow function ignores call/apply/bind
// Detached from object: regular function loses 'this'
const fn1 = john.sayName1;
fn1(); // undefined
const fn2 = john.sayName2;
fn2(); // John — arrow function retains original 'this'
Function, Method and Constructor calls in JavaScript?1. Functions: The simplest usages of function call:
function display(name) {
return "Hello " + name;
}
display("World"); // "Hello World"
2. Methods: in JavaScript are nothing more than object properties that are functions.
var obj = {
display : function() {
return "Hello " + this.name;
},
name: 'Minali Peri'
}
obj.display(); // "Hello Minali Peri"
3. Constructors: Like function and method, constructors are defined with function.
function Employee(name, age) {
this.name = name;
this.age = age;
}
var emp1 = new Employee('Drishya Sama', 28);
emp1.name; // "Drishya Sama"
emp1.age; // 28
Unlike function calls and method calls, a constructor call new Employee('Drishya Sama', 28) creates a new object and passes it as the value of this, and implicitly returns the new object as its result. The primary role of the constructor function is to initialize the object.
An arrow function is a shorter syntax for a function expression and does not have its own this, arguments, super, or new.target. These function are best suited for non-method functions, and they cannot be used as constructors.
Arrow functions in ES6 has two limitations:
When should not use Arrow Functions:
1. Object methods:
The counter object has two methods: current() and next(). The current() method returns the current counter value and the next() method returns the next counter value.
// Usin arrow function
const counter = {
count: 0,
next: () => ++this.count,
current: () => this.count
};
console.log(counter.next()); // NaN
2. Event handlers:
If we click the button, we would get a TypeError. It is because this is not bound to the button, but instead bound to its parent scope.
let button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
3. Prototype methods:
The this value in these next() and current() methods reference the global object. Since the this value inside the methods to reference the Counter object, it needs to use the regular functions instead
function Counter() {
this.count = 0;
}
Counter.prototype.next = () => {
return this.count;
};
Counter.prototype.current = () => {
return ++this.next;
}
4. Functions that use the arguments object:
Arrow functions don't have the arguments object. Therefore, if a function that uses arguments object, you cannot use the arrow function.
const concat = (separator) => {
let args = Array.prototype.slice.call(arguments, 1);
return args.join(separator);
}
⚝ Try this example on CodeSandbox
JavaScript function objects are used to define a piece of JavaScript code. This code can be called within a JavaScript code as and when required.
Javascript Function Objects Property:
| Name | Description |
|---|---|
| arguments | An array corresponding to the arguments passed to a function. |
| arguments.callee | Refers the currently executing function. |
| arguments.length | Refers the number of arguments defined for a function. |
| constructor | Specifies the function that creates an object. |
| length | The number of arguments defined by the function. |
| prototype | Allows adding properties to a Function object. |
In javaScript, functions can be stored as a variable inside an object or an array as well as it can be passed as an argument or be returned by another function. That makes function first-class function in JavaScript.
A first-class function means functions are treated as values — they can be:
const greet = function() { return "Hello"; }arr.map(fn)function outer() { return function inner() {} }const obj = { fn: () => {} }JavaScript fully supports first-class functions, which enables patterns like callbacks, higher-order functions, and closures.
Example 01: Assign a function to a variable
const message = function() {
console.log("Hello World!");
}
message(); // Invoke it using the variable
Example 02: Pass a function as an Argument
function sayHello() {
return "Hello, ";
}
function greeting(helloMessage, name) {
console.log(helloMessage() + name);
}
// Pass `sayHello` as an argument to `greeting` function
greeting(sayHello, "JavaScript!");
Example 03: Return a function
function sayHello() {
return function() {
console.log("Hello!");
}
}
Example 04: Using a variable
const sayHello = function() {
return function() {
console.log("Hello!");
}
}
const myFunc = sayHello();
myFunc();
Example 05: Using double parentheses
function sayHello() {
return function() {
console.log("Hello!");
}
}
sayHello()();
We are using double parentheses ()() to invoke the returned function as well.
⚝ Try this example on CodeSandbox
A Higher-Order function is a function that receives a function as an argument or returns the function as output.
For example, Array.prototype.map(), Array.prototype.filter() , Array.prototype.forEach() and Array.prototype.reduce() are some of the Higher-Order functions in javascript.
Example 01: .map()
const array = [10, 20, 30];
const result = array.map(function (item) {
return item * 2;
});
console.log(result); // [20, 40, 60]
Example 02: .filter()
const randomNumbers = [4, 11, 42, 14, 39];
const filteredArray = randomNumbers.filter((number) => {
return number > 15;
});
console.log(filteredArray); // [42, 39]
Example 03: .forEach()
const numbers = [28, 77, 45];
numbers.forEach((number) => {
console.log(number);
});
Example 04: .reduce()
const arrayOfNumbers = [10, 20, 30];
const sum = arrayOfNumbers.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
});
console.log("Sum: " + sum); // 60
⚝ Try this example on CodeSandbox
Unary function (i.e. monadic) is a function that accepts exactly one argument. It stands for single argument accepted by a function.
// Unary function
const unaryFunction = (number) => number + 10;
console.log(unaryFunction(10)); // 20
⚝ Try this example on CodeSandbox
Currying is the process of taking a function with multiple arguments and turning it into a sequence of functions each with only a single argument.
In other words, when a function, instead of taking all arguments at one time, takes the first one and return a new function that takes the second one and returns a new function which takes the third one, and so forth, until all arguments have been fulfilled.
// Normal function
const add = (a, b, c) => {
return a + b + c;
};
console.log(add(10, 10, 10)); // 30
// Currying function
const addCurry = (a) => {
return (b) => {
return (c) => {
return a + b + c;
};
};
};
console.log(addCurry(20)(20)(20)); // 60
Note: Curried functions are great to improve code re-usability and functional composition.
⚝ Try this example on CodeSandbox
Pure functions are functions that accept an input and returns a value without modifying any data outside its scope(Side Effects). Its output or return value must depend on the input/arguments and pure functions must return a value.
Example: Pure Function
It is a pure function because you always get a Hello <name> as output for the <name> pass as an input.
// Pure Function
function sayGreeting(name) {
return `Hello ${name}`;
}
console.log(sayGreeting("World"));
Example: Not Pure Function
The function's output now depends on an outer state called greeting. What if someone changes the value of the greeting variable to Hola? It will change the output of the sayGreeting() function even when you pass the same input.
let greeting = "Hello";
function sayGreeting(name) {
return `${greeting} ${name}`;
}
A function must pass two tests to be considered pure:
Benefits:
Memoization is a programming technique which attempts to increase a function's performance by caching its previously computed results.
Each time a memoized function is called, its parameters are used to index the cache. If the data is present, then it can be returned, without executing the entire function. Otherwise the function is executed and then the result is added to the cache.
// Memoized function to Add Number
const memoizedAdd = () => {
let cache = {};
return (number) => {
if (number in cache) {
console.log('Fetching from cache: ');
return cache[number];
}
else {
console.log('Calculating result: ');
let result = number + 10;
cache[number] = result;
return result;
}
}
}
// returned function from memoizedAdd
const sum = memoizedAdd();
console.log(sum(10)); // Calculating result: 20
console.log(sum(10)); // Fetching from cache: 20
⚝ Try this example on CodeSandbox
The arguments object is an Array-like object ( arguments ) accessible inside functions that contains the values of the arguments passed to that function.
Example:
/**
* Arguments Object
*/
function sum() {
let total = 0;
for (let i = 0, len = arguments.length; i < len; ++i) {
total += arguments[i];
}
return total;
}
sum(10, 20, 30); // returns 60
⚝ Try this example on CodeSandbox
The length property indicates the number of parameters expected by the function.
// function.length
function fun1() {}
console.log(fun1.length); // 0
function fun2(arg1, arg2) {}
console.log(fun2.length); // 2
⚝ Try this example on CodeSandbox
The bind() method creates a new function, when invoked, has the this sets to a provided value. The bind() method allows an object to borrow a method from another object without making a copy of that method. This is known as function borrowing in JavaScript.
Example:
/**
* bind() function
*/
const person = {
firstName: "Chhavi",
lastName: "Goswami",
fullName: function () {
return this.firstName + " " + this.lastName;
}
};
const member = {
firstName: "Vasuda",
lastName: "Sahota"
};
let fullName = person.fullName.bind(member);
console.log(fullName()); // Vasuda Sahota
⚝ Try this example on CodeSandbox
An anonymous function is a function without a name. Anonymous functions are commonly assigned to a variable name or used as a callback function.
Example 01: Anonymous function
let show = function () {
console.log("Anonymous function");
};
show();
Example 02: anonymous functions as arguments
setTimeout(function () {
console.log("Execute later after 1 second");
}, 1000);
Example 03: Immediately invoked function execution
const person = {
firstName: "Ayaan",
lastName: "Memon"
};
(function () {
console.log(person.firstName + " " + person.lastName); // Ayaan Memon
})(person);
Example 04: Arrow functions
let add = (a, b) => a + b;
add(10, 20); // 30
⚝ Try this example on CodeSandbox
function foo() {} and var foo = function() {}?1. Function Declaration:
Function declarations are evaluated upon entry into the enclosing scope, before any step-by-step code is executed. The function's name (foo) is added to the enclosing scope.
foo(); // Function Declaration Example!
function foo() {
console.log("Function Declaration Example!");
}
2. Function Expression:
Function expressions are evaluated as part of the step-by-step code, at the point where they appear. That one creates a function with no name, which it assigns to the foo variable.
foo(); // TypeError: foo is not a function
var foo = function() {
console.log(typeof foo); // function
};
console.log(typeof foo); // undefined
⚝ Try this example on CodeSandbox
Function Declarations:
A declared function is "saved for later use", and will be executed later, when it is invoked (called).
// Function declaration
function add(num1, num2) {
return num1 + num2;
}
function is only declared here. For using it, it must be invoked using function name. e.g add(10, 20);
Function Expression:
A function expression can be stored in a variable:
// Function expression
var add = function (num1, num2) {
return num1 + num2;
};
After a function expression has been stored in a variable, the variable can be used as a function. Functions stored in variables do not need function names. They are always invoked (called) using the variable name.
Difference:
Function declarations load before any code is executed while Function expressions load only when the interpreter reaches that line of code.var statement, function declarations are hoisted to the top of other code. Function expressions aren't hoisted, which allows them to retain a copy of the local variables from the scope where they were defined.Benefits of Function Expressions:
There are several different ways that function expressions become more useful than function declarations.
1. Function:
A function is a piece of code that is called by name and function itself not associated with any object and not defined inside any object. It can be passed data to operate on (i.e. parameter) and can optionally return value.
Example:
// Function
function message(msg) {
return msg;
}
// Call the function
message("Welcome to JavaScript");
Here, message() function call is not associated with object hence not invoked through any object.
2. Method:
A JavaScript method is a property of an object that contains a function definition. Methods are functions stored as object properties.
Example:
// Method
let employee = {
firstName: "Ajay",
lastName: "Nagi",
getName: function () {
return "Employe Name: " + this.firstName + " " + this.lastName;
}
};
// Call the method
console.log(employee.getName());
Here employee is an object and getName is a method which is associated with employee.
⚝ Try this example on CodeSandbox
Function binding ( .bind() ) is a method on the prototype of all functions in JavaScript. It allows to create a new function from an existing function, change the new function's this context, and provide any arguments you want the new function to be called with. The arguments provided to bind will precede any arguments that are passed to the new function when it is called.
Example:
// Function Binding
const person = {
firstName: "Nirupama",
lastName: "Randhawa",
getName: function () {
return this.firstName + " " + this.lastName;
}
};
const member = {
firstName: "Alisha",
lastName: "Chhabra"
};
let getName = person.getName.bind(member);
console.log(getName()); // Alisha Chhabra
⚝ Try this example on CodeSandbox
this works in JavaScript?The this keyword refers to an object. Which object depends on how this is being invoked (used or called). The this keyword refers to different objects depending on how it is used.
this refers to the object.this refers to the global object.this refers to the global object.this is undefined.this refers to the element that received the event.call(), apply(), and bind() can refer this to any object.Example:
// this keyword in object method
const person = {
firstName: "Nirupama",
lastName: "Randhawa",
getName: function () {
return this.firstName + " " + this.lastName;
}
};
Generator-Function:
A generator-function is defined like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return. The yield statement suspends function's execution and sends a value back to caller, but retains enough state to enable function to resume where it is left off. When resumed, the function continues execution immediately after the last yield run.
Syntax:
function* gen() {
yield 1;
yield 2;
yield 3;
...
}
Generator-Object:
Generator functions return a generator object. Generator objects are used either by calling the next method on the generator object or using the generator object in a "for in" loop.
Example:
// Generate Function
function* fun() {
yield 10;
yield 20;
yield 30;
}
// Calling the Generate Function
var gen = fun(); // returns a Generator object (doesn\'t run yet)
gen.next(); // { value: 10, done: false }
gen.next(); // { value: 20, done: false }
gen.next(); // { value: 30, done: false }
gen.next(); // { value: undefined, done: true }
⚝ Try this example on CodeSandbox
1. Generators/Yield:
Generators are objects created by generator functions — functions with an * (asterisk) next to their name. The yield keyword pauses generator function execution and the value of the expression following the yield keyword is returned to the generator's caller. It can be thought of as a generator-based version of the return keyword.
Example:
// Generator function
const generator = (function* () {
// waiting for .next()
const number = yield 5;
// waiting for .next()
console.log(number); // => 15
})();
console.log(generator.next()); // => { done: false, value: 5 }
console.log(generator.next(15)); // => { done: true, value: undefined }
2. Async/Await:
Async keyword is used to define an asynchronous function, which returns a AsyncFunction object.
Await keyword is used to pause async function execution until a Promise is fulfilled, that is resolved or rejected, and to resume execution of the async function after fulfillments. When resumed, the value of the await expression is that of the fulfilled Promise.
Key points:
Example:
// Async/Await
async function asyncFunction() {
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("resolved!"), 1000);
});
const result = await promise;
// wait till the promise resolves (*)
console.log(result); // "resolved!"
}
asyncFunction();
Generator and Async-await — Comparison:
⚝ Try this example on CodeSandbox
Two dates can be compared by converting them into numeric values using date.getTime() method to correspond to their time. Also,
the relational operators <, <=, >, >= can be used to compare JavaScript dates.
However, the equality operators ==, !=, ===, !== cannot be used to compare (the value of) dates because:
Example:
const date1 = new Date('2026-05-16T12:00:00');
const date2 = new Date('2026-05-16T12:00:00');
// Checking for equality
if (date1.getTime() === date2.getTime()) {
console.log("Both dates are exactly equal");
}
// Checking before or after
if (date1.getTime() < date2.getTime()) {
console.log("date1 is before date2");
}
⚝ Try this example on CodeSandbox
A closure is the combination of a function and the lexical environment within which that function was declared. i.e, it is an inner function that has access to the outer or enclosing function's variables.
Closure is useful in hiding implementation detail in JavaScript. In other words, it can be useful to create private variables or functions.
Lexical Scope:
In lexical scoping, free variables must belong to a parent scope. JavaScript uses lexical scoping — a function's scope is determined by where it is declared, not where it is called.
Example 01:
/**
* Lexical Scope
**/
function init() {
let name = "JavaScript closures"; // name is a local variable created by init
function displayName() {
// displayName() is the inner function, a closure
console.log(name); // use variable declared in the parent function
}
return displayName;
}
var closure = init();
closure(); // Output: "JavaScript closures"
As per the above code, the inner function displayName() has access to the variable name from the outer function init(), even after init() has returned. This is a closure.
⚝ Try this example on CodeSandbox
Example 02: Private Counter
A common use of closures is to create private state that cannot be accessed or modified directly from outside.
/**
* Closure - Private Counter
**/
function makeCounter() {
let count = 0; // private variable
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // Output: 2
// count is not directly accessible
console.log(typeof count); // Output: "undefined"
The variable count is encapsulated inside makeCounter and can only be modified through the returned methods.
A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.
Example:
/**
* Callback function
*/
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
function sayBye() {
console.log('Goodbye!');
}
greet('Alice', sayBye);
// Hello, Alice
// Goodbye!
Key points:
Promises or async/awaitNote: callbacks are often used to continue code execution after an asynchronous operation has completed — these are called asynchronous callbacks..
⚝ Try this example on CodeSandbox
Callback hell occurs when asynchronous operations are deeply nested inside one another, creating an unreadable "Pyramid of Doom"
Example:
// Callback hell
getUser(id, function(user) {
getOrders(user, function(orders) {
getDetails(orders[0], function(details) {
process(details, function(result) {
console.log(result); // deeply nested
});
});
});
});
Techniques for avoiding callback hell:
1. Promises — chain .then() instead of nesting:
getUser(id)
.then(user => getOrders(user))
.then(orders => getDetails(orders[0]))
.then(details => process(details))
.then(result => console.log(result))
.catch(err => console.error(err));
2. async/await — most readable, looks synchronous:
async function run() {
try {
const user = await getUser(id);
const orders = await getOrders(user);
const details = await getDetails(orders[0]);
const result = await process(details);
console.log(result);
} catch (err) {
console.error(err);
}
}
3. Named functions — extract callbacks into separate named functions:
function onDetails(details) { process(details).then(console.log); }
function onOrders(orders) { getDetails(orders[0]).then(onDetails); }
function onUser(user) { getOrders(user).then(onOrders); }
getUser(id).then(onUser);
4. Promise.all — run independent async operations in parallel:
const [users, orders] = await Promise.all([getUsers(), getOrders()]);
Note: Recommended approach: Use async/await for sequential logic and Promise.all for parallel operations.
JavaScript provides two built-in functions for URL encoding:
1. encodeURIComponent() — encodes a single URI component (query param value, path segment):
const param = 'hello world & name=John';
encodeURIComponent(param); // 'hello%20world%20%26%20name%3DJohn'
2. encodeURI() — encodes a full URL, preserving special URL characters (:, /, ?, #, &, =):
const url = 'https://example.com/search?q=hello world&lang=en';
encodeURI(url); // 'https://example.com/search?q=hello%20world&lang=en'
Key difference:
| Function | Encodes | Preserves |
|---|---|---|
encodeURIComponent | Everything except A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Nothing special |
encodeURI | Spaces and unsafe chars | : / ? # & = @ ! $ ' ( ) * + , ; ~ |
When to use which:
const base = 'https://example.com/search';
const query = 'hello world & more';
// Correct: encode only the param value
const url = `${base}?q=${encodeURIComponent(query)}`;
// https://example.com/search?q=hello%20world%20%26%20more
// Wrong: encodeURI won't encode & in query values
encodeURI(`${base}?q=${query}`);
// https://example.com/search?q=hello%20world%20&%20more ← & breaks the query
Rule of thumb: Use encodeURIComponent for query parameter values; use encodeURI for a complete URL.
The decodeURI() function is used to decode a Uniform Resource Identifier (URI) previously created by encodeURI().
1. decodeURIComponent() — decodes a single encoded URI component:
decodeURIComponent('hello%20world%20%26%20name%3DJohn');
// 'hello world & name=John'
2. decodeURI() — decodes a fully encoded URL, leaving valid URL structure characters intact:
decodeURI('https://example.com/search?q=hello%20world&lang=en');
// 'https://example.com/search?q=hello world&lang=en'
Example — reading query params:
const url = 'https://example.com/search?q=hello%20world%20%26%20more';
const raw = url.split('?q=')[1];
decodeURIComponent(raw); // 'hello world & more'
Modern alternative — use URLSearchParams which handles encoding/decoding automatically:
const params = new URLSearchParams('q=hello%20world&lang=en');
params.get('q'); // 'hello world'
JavaScript does not support traditional function overloading (unlike Java or C#) where multiple functions can have the exact same name but different parameters. If you define two functions with the same name, the second definition will completely overwrite the first one.
Example:
function greet(name) { return `Hello, ${name}`; }
function greet(name, title) { return `Hello, ${title} ${name}`; }
greet('Alex'); // 'Hello, undefined Alex' — second definition always wins
Ways to simulate overloading:
1. Overloading by Argument Count (arguments.length):
function calculateArea(...args) {
if (args.length === 1) {
return Math.PI * args[0] * args[0]; // Circle area: 1 argument (radius)
}
if (args.length === 2) {
return args[0] * args[1]; // Rectangle area: 2 arguments (width, height)
}
}
console.log(calculateArea(5)); // 78.54 (Circle)
console.log(calculateArea(4, 5)); // 20 (Rectangle)
2. Overloading by Data Type (typeof / instanceof)
function add(a, b) {
if (typeof a === 'string') return a + b; // string concat
if (typeof a === 'number') return a + b; // numeric add
}
add(1, 2); // 3
add('foo', 'bar'); // 'foobar'
3. Overloading using an Options Object
function createUser({ name, age = 18, role = 'user' } = {}) {
return { name, age, role };
}
createUser({ name: 'Alice', age: 20 });
createUser({ name: 'Bob', age: 30, role: 'Admin' });
4. Rest parameters for variable argument count:
function sum(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2); // 3
sum(1, 2, 3, 4); // 10
Recommended approach: Use the options object pattern for complex cases — it's readable, flexible, and self-documenting.
An IIFE (Immediately Invoked Function Expression) is a JavaScript function that is defined and executed immediately after its creation. It is a design pattern used to execute code immediately and isolate variables from the global scope.
Syntax:
(function () {
console.log('Runs immediately');
})();
// Arrow function version
(() => {
console.log('Also runs immediately');
})();
Example 01:
const result = (function () {
const message = 'Hello from IIFE';
return message;
})();
console.log(result); // 'Hello from IIFE'
console.log(typeof message); // 'undefined' — private to the IIFE
Example 02: Create a private scope
// Counter using IIFE
const counter = (function () {
let count = 0; // private
return {
increment() { return count++; },
value() { return count; }
};
})();
console.log(counter.increment()); // 0
console.log(counter.value()); // 1
//console.log(count); // ReferenceError: count is not defined
Use cases:
Data Privacy: Variables declared inside an IIFE cannot be accessed from the outside world. This protects your code from polluting the global scope.
Avoiding Naming Collisions: It prevents variables from accidentally overwriting other variables in third-party libraries or scripts.
State Preservation: Historically, it was used inside loops to lock in the current value of a variable during asynchronous operations.
Note:
With ES6 let/const and modules, IIFEs are less common today — block scoping and module scope solve the same problems. However, IIFEs are still useful for one-time initialization logic and encapsulating setup code.
Recursion is a technique in which a function calls itself until it reaches a base case (a condition that stops further calls). Every recursive function must have a base case to avoid infinite loops and stack overflow errors.
Example 01: Factorial
function factorial(n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive call
}
console.log(factorial(5)); // 120 (5 × 4 × 3 × 2 × 1)
console.log(factorial(0)); // 1
Example 02: Fibonacci
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(6)); // 8 (0, 1, 1, 2, 3, 5, 8)
Example 03: Flatten a nested array
function flattenArray(arr) {
return arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flattenArray(val)) : acc.concat(val),
[]);
}
console.log(flattenArray([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]
Recursion vs Iteration:
Note: For deeply nested structures, iterative solutions with an explicit stack are preferred over recursion to avoid stack-overflow errors.
A nested function is a function defined inside another function. The inner function has access to the outer function's variables and parameters through closure, but the outer function cannot access the inner function's variables.
Example:
function outerFunction(outerValue) {
const outerVar = 'I am outer';
function innerFunction(innerValue) {
// inner can access outer\'s scope
console.log(outerVar); // 'I am outer'
console.log(outerValue); // passed argument
console.log(innerValue); // inner argument
}
innerFunction('I am inner');
}
outerFunction('hello');
Use case — private helper functions:
function processOrder(order) {
function validate(o) {
return o.quantity > 0 && o.price > 0;
}
function calculateTotal(o) {
return o.quantity * o.price;
}
if (!validate(order)) throw new Error('Invalid order');
return calculateTotal(order);
}
console.log(processOrder({ quantity: 3, price: 15 })); // 45
The inner helpers validate and calculateTotal are private to processOrder and not accessible from outside.
A side effect is any observable change that a function makes to the state outside its own scope — such as modifying a global variable, mutating an argument, writing to the DOM, or making a network request.
Functions with side effects:
let count = 0;
function increment() {
count++; // modifies external state — side effect
}
increment();
console.log(count); // 1
Pure function (no side effects):
function add(a, b) {
return a + b; // no external mutation, same input always gives same output
}
Common sources of side effects:
| Source | Example |
|---|---|
| Mutating arguments | arr.push(item) inside a function |
| Global state change | Modifying window or module-level variables |
| I/O operations | console.log, network calls, file writes |
| DOM manipulation | document.getElementById('x').textContent = '...' |
Why it matters: Functions with side effects are harder to test, reason about, and reuse. Functional programming favors isolating side effects to the edges of a system while keeping the core logic pure.
Point-free style (also called tacit programming) is a way of defining functions without explicitly mentioning the arguments they operate on. Instead, functions are composed of other functions using combinators like map, filter, reduce, or a compose/pipe utility.
Without point-free:
const numbers = [1, 2, 3, 4, 5];
// explicitly names the argument n
const doubled = numbers.map(n => n * 2);
With point-free:
const double = x => x * 2;
// double is passed directly — no explicit argument mentioned
const doubled = numbers.map(double);
More complex example:
const trim = str => str.trim();
const toUpperCase = str => str.toUpperCase();
const words = str => str.split(' ');
// Point-free pipeline using a pipe utility
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const process = pipe(trim, toUpperCase, words);
console.log(process(' hello world ')); // ['HELLO', 'WORLD']
Point-free style leads to cleaner, more declarative code but can reduce readability when overused.
Function composition is the process of combining two or more functions where the output of one function becomes the input of the next. It is a core concept in functional programming.
compose applies functions right-to-left; pipe applies them left-to-right.
compose:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const add10 = x => x + 10;
const double = x => x * 2;
const square = x => x * x;
// square( double( add10(5) ) ) => square(double(15)) => square(30) => 900
const transform = compose(square, double, add10);
console.log(transform(5)); // 900
pipe (left-to-right):
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// add10(5) => double(15) => square(30) => 900
const transform = pipe(add10, double, square);
console.log(transform(5)); // 900
Real-world example:
const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s+/g, '-');
const slugify = pipe(trim, toLower, removeSpaces);
console.log(slugify(' Hello World ')); // 'hello-world'
Lexical scope (also called static scope) means that the scope of a variable is determined at author time — by where the function is written in the source code, not where it is called from. JavaScript uses lexical scoping exclusively. When the JavaScript engine (V8) parses the source, it builds a scope chain based on the physical nesting of functions and blocks; this chain never changes at runtime.
Dynamic scope, by contrast, resolves variable references based on the call stack at runtime. Languages like Bash and early Lisp dialects use dynamic scope. JavaScript has no dynamic scope (though this is sometimes confused for it because it is determined by the call site — this binding is separate from scope).
Key rules of lexical scope:
ReferenceError is thrown (in strict mode) or an implicit global is created (sloppy mode).// Lexical scope example
const greeting = 'Hello'; // global scope
function outer() {
const name = 'Alice'; // outer function scope
function inner() {
const punctuation = '!'; // inner function scope
// inner can see: punctuation, name, greeting (chain lookup)
console.log(`${greeting}, ${name}${punctuation}`);
}
inner(); // 'Hello, Alice!'
// console.log(punctuation); // ReferenceError — outer cannot see inner\'s variables
}
outer();
Real-World Use Case:
React's hook system relies on lexical scope. When you write a custom hook like useCounter, the state variables declared inside it are lexically scoped to that hook invocation — they cannot bleed into unrelated components, making stateful logic safely composable and predictable in a large codebase.
var, let, and const with respect to scope.| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes (as undefined) | Yes (TDZ — not initialised) | Yes (TDZ — not initialised) |
| Re-declarable | Yes | No | No |
| Re-assignable | Yes | Yes | No (binding; object contents can mutate) |
| Global property | Creates window.x | No | No |
Block scope means the variable only exists within the nearest enclosing {} — an if, for, while, or plain block. var ignores block boundaries and is scoped to the enclosing function (or the global object if outside any function).
function demonstrateScope() {
if (true) {
var funcScoped = 'var'; // hoisted to function top
let blockScoped = 'let'; // lives only inside this if-block
const immutable = 'const'; // same block scope as let
}
console.log(funcScoped); // 'var'
// console.log(blockScoped); // ReferenceError
// console.log(immutable); // ReferenceError
}
// Classic var-in-loop bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 0); // prints 3, 3, 3
}
// Fixed with let (each iteration gets its own binding)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 0); // prints 0, 1, 2
}
Real-World Use Case:
In large React/Node.js codebases, var inside for loops iterating over async operations (e.g., firing multiple API calls) was a notorious source of race-condition bugs because all callbacks shared the same i. Migrating to let (or closure-capturing patterns) eliminated entire categories of production bugs. ESLint's no-var rule enforces this at scale.
In ES6, let bindings are not subject to "variable hoisting", which means that let declarations do not move to the top of the current execution context.
Referencing the variable in the block before the initialization results in a ReferenceError (contrary to a variable declared with var, which will just have the undefined value). The variable is in a "temporal dead zone" from the start of the block until the initialization is processed.
console.log(x); // undefined — var is hoisted and initialised to undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
// TDZ also applies inside blocks
{
// TDZ for 'z' starts here ↓
// console.log(z); // ReferenceError
let z = 42; // TDZ ends here, z is now initialised
console.log(z); // 42
}
Real-World Use Case:
TDZ prevents a subtle bug in module initialisation cycles. If module A imports from module B and module B imports from module A, const exports are protected by TDZ, causing a clear ReferenceError at the problematic access point rather than silently producing undefined computations that corrupt data — making circular-dependency bugs far easier to diagnose in large monorepo setups.
A closure is a function that retains a reference to its lexical environment (the scope in which it was defined) even after that outer scope has finished executing. In V8, when a function is created, the engine attaches a hidden [[Environment]] slot pointing to the current scope's environment record. As long as the inner function is reachable, the garbage collector keeps the outer environment record alive.
Practically: closures "close over" variables — not values. They hold a live reference to the variable binding, so mutations to that variable are visible inside the closure.
function makeCounter(initialValue = 0) {
let count = initialValue; // this variable is closed over
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = initialValue; return count; },
value() { return count; },
};
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.value()); // 11
// count is NOT accessible from outside
// console.log(count); // ReferenceError
Real-World Use Case:
The Redux store implementation (createStore) uses closures to encapsulate currentState, currentReducer, and listeners. External code can only interact with state through the returned dispatch, getState, and subscribe functions — a closure-based module pattern that enforces immutable, predictable state management at scale.
The Module Pattern (and its IIFE variant) uses closures to create private state — something JavaScript classes did not natively support before ES2022 private fields (#). An IIFE runs once, defines private variables, and returns a public API. Only the returned methods can access the private variables via closure.
Example:
// Classic Module Pattern (IIFE-based)
const BankAccount = (() => {
// Private state — not accessible from outside
let _balance = 0;
const _history = [];
function _log(action, amount) {
_history.push({ action, amount, balance: _balance, ts: Date.now() });
}
// Public API
return {
deposit(amount) {
if (amount <= 0) throw new RangeError('Amount must be positive');
_balance += amount;
_log('deposit', amount);
return this;
},
withdraw(amount) {
if (amount > _balance) throw new Error('Insufficient funds');
_balance -= amount;
_log('withdrawal', amount);
return this;
},
getBalance() { return _balance; },
getHistory() { return [..._history]; }, // defensive copy
};
})();
BankAccount.deposit(500).deposit(200).withdraw(100);
console.log(BankAccount.getBalance()); // 600
console.log(BankAccount._balance); // undefined — truly private
Real-World Use Case:
Before ES modules became ubiquitous, large libraries like jQuery, Lodash, and early Angular 1.x used the Module Pattern (IIFE + closure) to avoid polluting the global namespace and to encapsulate internal state. Today, ES modules provide file-level encapsulation, but the closure-based pattern is still used inside modules for class-alternative encapsulation and for factories returned from hooks or composables.
A closure memory leak occurs when a closure unintentionally keeps a large object (such as a DOM node, a large dataset, or a complete HTTP response) in memory by holding a reference in its closed-over scope long after the object is needed. The V8 garbage collector cannot reclaim the memory because the closure is still reachable.
Common patterns that cause leaks:
setInterval callbacks closing over large objects run indefinitely.// Leak: interval callback closes over largeData forever
function startPolling() {
const largeData = new Array(100_000).fill('payload');
const id = setInterval(() => {
console.log(largeData.length); // largeData is never freed
}, 1000);
// Missing: return id so the caller can clearInterval(id)
}
// Fix: return cleanup, or null out the reference
function startPollingFixed() {
const largeData = new Array(100_000).fill('payload');
const id = setInterval(() => {
process(largeData);
}, 1000);
return () => clearInterval(id); // caller must invoke this to free memory
}
const stopPolling = startPollingFixed();
// Later, when no longer needed:
stopPolling(); // interval cleared, largeData eligible for GC
React-specific pattern:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort(); // cleanup prevents stale closure + memory leak
}, []);
Real-World Use Case:
In Single Page Applications (SPAs), route navigation causes component unmounting. Event listeners or WebSocket message handlers registered on mount but not removed on unmount keep the entire component closure alive for the application's lifetime, causing memory growth that crashes long-running sessions (e.g., trading dashboards, kiosk apps). React's useEffect cleanup function pattern was designed specifically to address this.
An Immediately Invoked Function Expression (IIFE) is a function that is defined and called in a single expression. Its primary purpose is to create an isolated scope, preventing variable declarations from leaking into the surrounding (often global) scope.
(function() { /* body */ })();
// or with arrow functions:
(() => { /* body */ })();
When to still use IIFEs over ES modules:
type="module" and need scope isolation.window.MyLib while avoiding global pollution.Example:
// UMD wrapper (simplified) — the output of many bundlers
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? module.exports = factory() // CommonJS (Node.js)
: typeof define === 'function' && define.amd
? define(factory) // AMD (RequireJS)
: (global.MyLib = factory()); // Browser global
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
const VERSION = '1.0.0'; // private
return { VERSION, greet: (name) => `Hello, ${name}` }; // public API
}));
Real-World Use Case:
Webpack wraps each module in an IIFE in its legacy output mode to simulate module scope in environments lacking native ES module support (Internet Explorer, older Node.js). When targeting modern environments, Webpack and Rollup emit native ES modules instead, removing the IIFE overhead. Understanding this is essential when configuring output.libraryTarget in webpack configs for distributing npm packages.
When a variable is referenced, the JavaScript engine performs a scope chain lookup: it starts at the current scope's environment record and walks up through enclosing records until the variable is found or the global scope is exhausted.
V8 optimises this through:
Performance implication: deeply nested closures that close over many large variables create long-lived heap contexts. Flattening the closure chain or copying only the needed values can improve GC throughput.
Example:
// Unnecessarily deep scope chain — engine must walk up 3 levels on each access
function level1() {
const config = loadHeavyConfig(); // heap-allocated context (closed over)
function level2() {
function level3() {
return config.timeout; // 3-level chain lookup
}
return level3;
}
return level2;
}
// Flatten: extract only needed value to the immediate scope
function level1Optimised() {
const { timeout } = loadHeavyConfig(); // only close over what is needed
function level2() {
function level3() {
return timeout; // 1 var, still 3-level chain but smaller context object
}
return level3;
}
return level2;
}
Real-World Use Case:
In Node.js request handlers that process millions of requests per minute (e.g., an Express.js API gateway), middleware functions close over the app config object. Profiling with --prof (V8 tick profiler) or Chrome DevTools' flame graph reveals when heap contexts from closures apply GC pressure. The optimisation is to destructure only required config fields at the module level — keeping the closure context objects small and promoting faster minor GC cycles.
this) in JavaScript?These two concepts are frequently confused but are entirely independent mechanisms:
| Scope | Context (this) | |
|---|---|---|
| Determined by | Where code is written (lexical) | How a function is called (dynamic) |
| Stores | Variable bindings | The object a function is operating upon |
| Changed by | Impossible at runtime | call, apply, bind, arrow functions, class fields |
| Applies to | All identifiers | Only the this keyword |
Arrow functions are the intersection: they do not create their own this binding — they inherit this lexically from the enclosing scope at definition time. This makes them behave like closures for this.
Example:
const timer = {
label: 'Countdown',
seconds: 3,
// Regular function: `this` is determined by caller (lost in setTimeout callback)
startBroken() {
setTimeout(function () {
console.log(this.label); // undefined — `this` is global/undefined (strict mode)
}, 1000);
},
// Arrow function: `this` inherited lexically from startFixed\'s scope
startFixed() {
setTimeout(() => {
console.log(this.label); // 'Countdown'
}, 1000);
},
// Explicit bind alternative
startWithBind() {
setTimeout(function () {
console.log(this.label); // 'Countdown'
}.bind(this), 1000);
},
};
timer.startFixed();
Real-World Use Case:
React class components historically required .bind(this) in the constructor for event handlers because a regular function passed as a JSX prop loses its this binding. The shift to arrow function class fields (handleClick = () => { ... }) and then to functional components with hooks eliminated this entire category of this-related bugs — demonstrating that understanding the scope vs. context distinction is foundational to making informed decisions about component architecture.
Hoisting is a JavaScript mechanism where variable, function, class, and import declarations are conceptually moved to the top of their containing scope during the compilation phase, before the code executes. Hoisting allows functions to be safely used in code before they are declared.
Example 01: Variable Hoisting
console.log(message); // output: undefined
var message = "The variable Has been hoisted";
The above code looks like as below to the interpreter,
var message;
console.log(message);
message = "The variable Has been hoisted";
Example 02: let and const hoisting
All declarations (function, var, let, const and class) are hoisted in JavaScript, while the var declarations are initialized with undefined, but let and const declarations remain uninitialized.
console.log(x);
let x = 10;
// Output: ReferenceError: x is not defined
Example 03: Function Hoisting
One of the advantages of hoisting is that it lets you use a function before you declare it in your code.
getName("Sadhika Sandal");
function getName(name) {
console.log("Hello " + name);
}
Output:
Hello Sadhika Sandal
They will only get initialized when their lexical binding (assignment) is evaluated during runtime by the JavaScript engine. This means we can't access the variable before the engine evaluates its value at the place it was declared in the source code. This is what we call Temporal Dead Zone (TDZ), A time span between variable creation and its initialization where they can't be accessed.
What gets hoisted and how:
| Declaration | Hoisted? | Initialised to |
|---|---|---|
var | Yes | undefined |
let | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
const | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
function declaration | Yes | Full function body |
function expression (var f = function(){}) | Partially | var f hoisted as undefined; body is not hoisted |
class declaration | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
import | Yes | Fully evaluated before module body runs |
Real-World Use Case:
Node.js module systems and build tools like Webpack rely on function-declaration hoisting to allow modules to call utility functions defined later in the same file — enabling a readable "public API at the top, implementation details below" code style. However, misunderstanding var hoisting is responsible for a large class of production bugs in legacy jQuery/Angular 1 codebases, which is why the Airbnb ESLint config bans var and mandates let/const.
Let us take the following function expression
var foo = function foo() {
return 12;
}
In JavaScript var-declared variables and functions are hoisted. Let us take function hoisting first. Basically, the JavaScript interpreter looks ahead to find all the variable declaration and hoists them to the top of the function where It is declared. For example:
foo(); // Here foo is still undefined
var foo = function foo() {
return 12;
};
The code above behind the scene look something like this:
var foo = undefined;
foo(); // Here foo is undefined
foo = function foo() {
// Some code stuff
}
var foo = undefined;
foo = function foo() {
// Some code stuff
}
foo(); // Now foo is defined here
function foo() {}): The entire function — name and body — is hoisted. The function is callable anywhere in its scope, even before the line it appears on.const foo = function() {}): Only the variable declaration is hoisted (if var); the assignment — the function value itself — is not. Calling it before the line throws TypeError (not ReferenceError) because the variable exists but holds undefined.const foo = () => {}): Same as function expression — it is always assigned to a variable, so the same rules apply.Example:
// 1. Function Declaration — fully hoisted
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
// 2. Function Expression with var — var hoisted as undefined
console.log(multiply); // undefined
// console.log(multiply(2, 3)); // TypeError: multiply is not a function
var multiply = function (a, b) {
return a * b;
};
// 3. Function Expression with let — TDZ
// console.log(divide(10, 2)); // ReferenceError
const divide = (a, b) => a / b;
// 4. Practical gotcha: conditional function declaration (avoid — browser-dependent)
if (true) {
function conditionalFn() { return 'inside if'; }
}
// Behaviour in strict mode: conditionalFn is block-scoped to the if-block
// Behaviour in sloppy mode: varies by browser — avoid this pattern entirely
Real-World Use Case:
In large React component files, developers often define helper arrow functions (const formatDate = (d) => ...) after the component function that uses them. Because the component is a const arrow function itself, calling formatDate inside it only works if formatDate is defined above — otherwise a ReferenceError occurs at module load time. Understanding hoisting rules guides file organisation: place utility declarations before their consumers, or use function declarations when ordering flexibility matters.
Class declarations are hoisted to the top of their block scope — like let and const — but they are not initialised until the declaration is evaluated. This means they sit in the Temporal Dead Zone (TDZ) and accessing a class before its declaration throws a ReferenceError, not a TypeError. This is a deliberate choice to prevent confusing inheritance hierarchies from being constructed before base classes are fully defined.
Example:
// Class in TDZ — accessing before declaration throws ReferenceError
// const p = new Person('Alice'); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, I am ${this.name}`;
}
}
const p = new Person('Alice'); //
console.log(p.greet()); // 'Hi, I am Alice'
// Class expression — same as function expression: name is in TDZ if const/let
const Animal = class {
constructor(species) {
this.species = species;
}
};
const dog = new Animal('Canis lupus'); //
Practical implication — class vs. function constructor:
// Function constructor: hoisted entirely — can be used before declaration
const f = new Foo(); //
function Foo() { this.x = 1; }
// Class: TDZ — cannot be used before declaration
// const b = new Bar(); // ReferenceError
class Bar { constructor() { this.x = 1; } }
Real-World Use Case:
When splitting large Node.js services into files with circular require() references, class-based modules that are accessed during the require() cycle before the class declaration evaluates throw ReferenceError. Understanding the TDZ for classes helps engineers design module load order carefully — or avoid circular dependencies by refactoring to dependency injection patterns (e.g., passing the class as a parameter rather than importing it at the module level).
let/const blocks, and how does it affect switch statements?let and const are block-scoped and hoisted within their block to the TDZ. A switch statement's {} is a single block — all case clauses share the same block scope. This causes a subtle bug: a let declared in one case is technically in scope (hoisted) for all other cases in the same switch block, causing a SyntaxError if another case attempts to declare the same name.
Example:
// SyntaxError: Identifier 'result' has already been declared
function getLabel(status) {
switch (status) {
case 'active':
let result = 'User is active'; // hoisted to top of switch block
return result;
case 'inactive':
let result = 'User is inactive'; // SyntaxError — same block, same name
return result;
}
}
// Fix 1: wrap each case in its own block
function getLabelFixed(status) {
switch (status) {
case 'active': {
let result = 'User is active';
return result;
}
case 'inactive': {
let result = 'User is inactive'; // own block — no conflict
return result;
}
default:
return 'Unknown';
}
}
// Fix 2: prefer object lookup over switch for simple mappings
const STATUS_LABELS = {
active: 'User is active',
inactive: 'User is inactive',
};
const getLabel2 = (status) => STATUS_LABELS[status] ?? 'Unknown';
Real-World Use Case:
This exact bug has appeared in production Redux reducers where developers switched from var to let during a linting migration. All case branches in a reducer's switch share one block, so re-declaring let draft in multiple cases causes a SyntaxError caught only at parse time — blocking the entire app from loading. The standard Redux Toolkit solution is to use Immer-based createSlice instead, which replaces the switch reducer pattern entirely.
var declarations and function declarations with the same name?When multiple var declarations or function declarations share the same identifier in the same scope, the engine applies specific precedence rules during the compilation phase:
var declarations are merged into one — only one undefined slot is allocated; whichever assignment executes last wins at runtime.var declarations of the same name during hoisting — the function body wins over var's undefined.Example:
// What does the programmer see:
var x = 1;
function x() { return 'fn'; }
var x = 2;
// What the engine processes during compilation:
// 1. var x → undefined (registered)
// 2. function x → x = function() { return 'fn'; } (overwrites var)
// 3. (execution phase) x = 1
// 4. (execution phase) x = 2
console.log(x); // 2 — last assignment wins
// Multiple function declarations — last one wins
function greet() { return 'Hello'; }
function greet() { return 'Hi'; } // overwrites the first
console.log(greet()); // 'Hi'
// var + function same name — function hoisted OVER var during compilation
console.log(typeof answer); // 'function' (not 'undefined')
var answer = 42;
function answer() { return 'forty-two'; }
console.log(answer); // 42 — runtime assignment overrides the function
Why this matters architecturally:
In large legacy codebases (pre-modules), concatenating multiple script files together frequently caused silent name collisions of this kind. Two files each declaring var config = ... or function init() {} would silently overwrite each other. The last file in the bundle always won, causing intermittent and environment-specific bugs that only appeared in production where file concatenation order differed from the development setup.
Real-World Use Case:
This collision class of bugs was a primary driver for the adoption of the Module Pattern (IIFE-based), then AMD/CommonJS, and ultimately ES modules — each solution adding a layer of scope isolation to prevent hoisting-based name collisions. Modern bundlers (Webpack, Rollup) assign each module its own scope by wrapping it, making cross-module var collision impossible. Understanding this history explains why "use strict" and ES modules are defaults in all modern toolchains.
Arrow functions (=>) introduced in ES2015 are a concise syntax for writing functions, but they are not simply shorthand for function. They have fundamental semantic differences:
| Feature | Regular Function | Arrow Function |
|---|---|---|
this binding | Dynamic — determined by call site | Lexical — inherits from enclosing scope |
arguments object | Available | Not available (use rest params) |
new (constructor) | Can be used | Throws TypeError |
prototype property | Has one | undefined |
super / new.target | Own binding | Inherited from enclosing scope |
| Method shorthand | Usable (with caution) | Avoid for object methods needing this |
When NOT to use arrow functions:
this (obj.method() — arrow's this is the enclosing scope, not obj).new.this to refer to the DOM element (element.addEventListener('click', () => {}) — this is the outer context, not the element).function*.arguments object.// --- Concise bodies ---
const double = x => x * 2; // single param, expression body
const add = (a, b) => a + b; // multiple params
const toUpper = str => ({ upper: str.toUpperCase() }); // returning an object literal — wrap in ()
// --- `this` lexical binding ---
class Timer {
constructor() {
this.ticks = 0;
}
start() {
// Arrow function: `this` is the Timer instance (lexical)
this.interval = setInterval(() => {
this.ticks++;
console.log(this.ticks);
}, 1000);
}
stop() {
clearInterval(this.interval);
}
}
// --- When NOT to use arrow ---
const counter = {
count: 0,
// Arrow: `this` is the module/global scope, not counter
incrementBad: () => { counter.count++; },
// Method shorthand: `this` is counter
incrementGood() { this.count++; },
};
// Arrow as constructor
// const p = new (() => {}); // TypeError: (intermediate value) is not a constructor
// No own `arguments`
const fn = () => {
// console.log(arguments); // ReferenceError in strict mode / module scope
};
const fn2 = (...args) => args; // Use rest params instead
Real-World Use Case:
React's functional components and hooks use arrow functions almost exclusively for event handlers passed as JSX props (onClick={() => dispatch(action)}). Because these arrow functions inherit this from the component scope (which in a functional component is irrelevant — there is no this), the lexical binding is harmless and the concise syntax reduces boilerplate. Conversely, in Express.js route handlers or Mongoose middleware that rely on this referring to the document/request, using arrow functions is a common production bug — the official Mongoose docs explicitly warn against it.
Template literals (backtick strings) introduced in ES2015 offer four key capabilities over traditional string literals:
${expr}.\n needed.${} including ternaries, function calls, and arithmetic.// 1. Interpolation
const user = { name: 'Alice', score: 42 };
console.log(`Player: ${user.name}, Score: ${user.score * 2}`); // Player: Alice, Score: 84
// 2. Multi-line
const html = `
<article>
<h1>${user.name}</h1>
<p>Score: ${user.score}</p>
</article>
`.trim();
// 3. Expressions inside ${}
const items = ['apple', 'banana', 'cherry'];
console.log(`You have ${items.length} item${items.length !== 1 ? 's' : ''}.`);
// 'You have 3 items.'
// 4. Tagged template literals
function highlight(strings, ...values) {
return strings.reduce((result, str, i) =>
result + str + (values[i] !== undefined ? `<mark>${values[i]}</mark>` : ''),
''
);
}
const product = 'JavaScript';
const year = 2015;
console.log(highlight`${product} was standardised in ${year}.`);
// '<mark>JavaScript</mark> was standardised in <mark>2015</mark>.'
// SQL injection prevention with tagged templates (concept)
function sql(strings, ...values) {
const sanitised = values.map(v => sanitiseInput(v)); // escape each interpolated value
return strings.reduce((q, str, i) => q + str + (sanitised[i] ?? ''), '');
}
function sanitiseInput(v) { return String(v).replace(/'/g, "''"); }
const userId = "1' OR '1'='1"; // SQL injection attempt
const query = sql`SELECT * FROM users WHERE id = '${userId}'`;
// SELECT * FROM users WHERE id = '1'' OR ''1''=''1' — safely escaped
Real-World Use Case:
The styled-components and emotion CSS-in-JS libraries are built entirely on tagged template literals. When you write styled.div`color: ${props => props.primary ? 'blue' : 'grey'};`, the library's tag function receives the static CSS parts and the dynamic value functions, generates a unique class name, injects a <style> rule, and tracks theme changes — all without any custom parser. This demonstrates that tagged templates can implement fully featured DSLs (CSS, GraphQL, SQL, i18n) natively in JavaScript with zero dependencies.
Destructuring is a syntax for unpacking values from arrays or properties from objects into distinct variables in a single statement. It is purely syntactic sugar — the engine converts it to a series of property accesses and assignments.
// ── Object destructuring ──────────────────────────────────────────────────────
const user = { id: 1, name: 'Alice', role: 'admin', address: { city: 'NYC', zip: '10001' } };
// Basic
const { id, name } = user;
// Rename (alias)
const { name: username, role: userRole } = user;
console.log(username, userRole); // 'Alice', 'admin'
// Default values (used when property is undefined)
const { theme = 'light', lang = 'en' } = user;
console.log(theme, lang); // 'light', 'en'
// Nested destructuring
const { address: { city, zip } } = user;
console.log(city, zip); // 'NYC', '10001'
// Rest in object destructuring
const { id: userId, ...rest } = user;
console.log(userId); // 1
console.log(rest); // { name: 'Alice', role: 'admin', address: {...} }
// ── Array destructuring ───────────────────────────────────────────────────────
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
const [r, , b] = rgb; // skip elements with empty commas
const [first, ...others] = rgb; // rest in array
// Swap variables without temp
let a = 1, bVal = 2;
[a, bVal] = [bVal, a];
console.log(a, bVal); // 2, 1
// Default values
const [x = 0, y = 0, z = 0] = [10, 20];
console.log(x, y, z); // 10, 20, 0
// ── Function parameter destructuring ─────────────────────────────────────────
function renderCard({ title, subtitle = 'No subtitle', tags = [] } = {}) {
return `<h1>${title}</h1><h2>${subtitle}</h2><ul>${tags.map(t => `<li>${t}</li>`).join('')}</ul>`;
}
renderCard({ title: 'Hello', tags: ['js', 'es6'] });
renderCard(); // uses defaults — the `= {}` prevents TypeError when called with no args
// ── Mixed / deeply nested ─────────────────────────────────────────────────────
const apiResponse = {
data: {
users: [
{ id: 1, profile: { avatar: 'alice.png' } },
{ id: 2, profile: { avatar: 'bob.png' } },
],
},
};
const { data: { users: [{ id: firstId, profile: { avatar: firstAvatar } }] } } = apiResponse;
console.log(firstId, firstAvatar); // 1, 'alice.png'
Real-World Use Case:
React's useState hook returns a two-element array precisely to enable array destructuring with custom naming: const [count, setCount] = useState(0). This pattern lets each call site choose its own variable names without the library needing to know them. Similarly, React component props are universally destructured in the function signature (function Button({ label, onClick, disabled = false }) {}), reducing prop-drilling verbosity and making prop contracts immediately visible — a pattern enforced by most team style guides and ESLint configs.
Both use the ... syntax but serve opposite purposes:
Rest (...params) | Spread (...iterable) | |
|---|---|---|
| Position | Last parameter in a function signature | Call site, array literal, or object literal |
| Purpose | Collects multiple arguments into an array | Expands an iterable/object into individual elements |
| Result | One Array | Multiple values |
// ── Rest parameter ───────────────────────────────────────────────────────────
function sum(...numbers) { // collects all args into an array
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
function log(level, ...messages) { // first arg fixed, rest collected
console[level](messages.join(' '));
}
log('warn', 'Disk', 'usage', 'high'); // console.warn('Disk usage high')
// ── Spread operator ──────────────────────────────────────────────────────────
// 1. Expanding arrays
const nums = [1, 2, 3];
const more = [...nums, 4, 5]; // [1, 2, 3, 4, 5]
const copy = [...nums]; // shallow copy
// 2. Spreading into a function call
console.log(Math.max(...nums)); // 3
// 3. Merging / cloning objects (shallow)
const defaults = { theme: 'light', lang: 'en', debug: false };
const userPrefs = { theme: 'dark', fontSize: 14 };
const config = { ...defaults, ...userPrefs }; // right side wins on key collision
// { theme: 'dark', lang: 'en', debug: false, fontSize: 14 }
// 4. Spreading a string into characters
const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
// 5. Converting NodeList / Set to Array
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]
Real-World Use Case:
Redux reducers use object spread to produce new state immutably without mutating the existing state object — a requirement of Redux's change-detection mechanism: return { ...state, isLoading: false, data: action.payload }. This replaces Object.assign({}, state, { isLoading: false }) with far more readable syntax. The V8 engine optimises spread-based object cloning with the same hidden-class mechanisms as Object.assign, so there is no meaningful performance difference in most application code.
Map and Set — how do they differ from plain objects and arrays, and when should you use them?Set is a collection of unique values of any type. It maintains insertion order and provides O(1) has() lookup (hash-based internally in V8).
Map is a collection of key-value pairs where keys can be any type (including objects, functions, and NaN). Plain objects only allow string/Symbol keys.
| Feature | Object | Map |
|---|---|---|
| Key types | String / Symbol only | Any value (including objects) |
| Ordered | Not guaranteed (though V8 preserves insertion order for string keys in practice) | Insertion order guaranteed by spec |
| Size | Object.keys(obj).length — O(n) | map.size — O(1) |
| Iteration | for...in (includes prototype) / Object.entries() | for...of, forEach, direct iterators |
| Prototype pollution | Possible (e.g., __proto__) | Not possible |
| JSON serialisable | Yes | (needs custom serialiser) |
| Feature | Array | Set |
|---|---|---|
| Uniqueness | No — duplicates allowed | Yes — duplicates silently ignored |
has / membership | indexOf / includes — O(n) | set.has() — O(1) |
| Ordered | Index-based | Insertion-order |
// ── Set ──────────────────────────────────────────────────────────────────────
const visited = new Set();
visited.add('/home');
visited.add('/about');
visited.add('/home'); // duplicate — silently ignored
console.log(visited.size); // 2
console.log(visited.has('/about')); // true (O(1))
// Deduplicate an array
const tags = ['js', 'css', 'js', 'html', 'css'];
const uniqueTags = [...new Set(tags)]; // ['js', 'css', 'html']
// ── Map ──────────────────────────────────────────────────────────────────────
const cache = new Map();
// Object as key (impossible with plain objects as keys)
const req1 = { url: '/api/users' };
const req2 = { url: '/api/posts' };
cache.set(req1, { data: [1, 2, 3], ts: Date.now() });
cache.set(req2, { data: ['a', 'b'], ts: Date.now() });
console.log(cache.get(req1).data); // [1, 2, 3]
console.log(cache.size); // 2
// Iteration
for (const [key, value] of cache) {
console.log(key.url, value.data);
}
// Convert to / from plain object
const mapFromObj = new Map(Object.entries({ a: 1, b: 2 }));
const objFromMap = Object.fromEntries(mapFromObj);
Real-World Use Case:
WeakMap (a variant of Map with weak references and object-only keys) is used by React's Concurrent Mode internals to associate component fiber metadata with DOM nodes without preventing garbage collection of unmounted components. Set is used in many state management libraries (Zustand, Jotai) to track subscriber callbacks: O(1) add/delete/has operations make subscription management significantly faster than array-based alternatives at scale (thousands of subscribers in large dashboards).
undefined vs. null?ES6 default parameters allow function parameters to have a fallback value used when the argument is undefined or when no argument is provided. Crucially, passing null does not trigger the default — null is an explicit value meaning "no object here", while undefined means "value not provided".
Default expressions are evaluated lazily — they run each time the function is called with a missing argument, not once at function definition time. This allows defaults that reference other parameters or call functions.
// Basic defaults
function createUser(name, role = 'viewer', createdAt = new Date()) {
return { name, role, createdAt };
}
createUser('Alice'); // role: 'viewer', createdAt: <now>
createUser('Bob', 'admin'); // role: 'admin'
createUser('Carol', undefined, new Date('2024-01-01')); // role: 'viewer' (undefined triggers default)
createUser('Dave', null); // role: null — null does NOT trigger default
// Default referencing earlier parameter
function range(start, end = start + 10) {
return Array.from({ length: end - start }, (_, i) => start + i);
}
console.log(range(5)); // [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
console.log(range(1, 4)); // [1, 2, 3]
// Default calling a function (evaluated per call)
let callCount = 0;
function getDefault() { return ++callCount; }
function track(id = getDefault()) {
Truncated — view the full README on GitHub.
339 followers · starred Feb 2022
248 followers · starred Apr 2021
19 followers · starred Aug 2024
168 followers · starred Aug 2021
JavaScript Basics
See the codeClick ★ if you like the project. Your contributions are heartily ♡ welcome.
Focus: Syntax, basic logic, and standard data handling.
Focus: ES6+ features, DOM, and common "trick" concepts.
Focus: Asynchronous operations, performance, and internal engine mechanics. [4]
Focus: Scalability, security, and low-level optimization.
Focus: Code review, team standards, architectural decisions, and engineering best practices for leading a development team.
Focus: System-level design, scalability, security posture, micro-frontend, and cross-team JavaScript architecture.
1. Template Strings:
Template literals are string literals allowing embedded expressions.
Benefits:
// String Substitution
let name = `Abhinav Sharma`;
console.log(`Hi, ${name}`); // Output: "Hi, Abhinav Sharma"
// Multiline String
let msg = `Hello \n
World`;
console.log(`${msg}`); // Output: "Hello World"
⚝ Try this example on CodeSandbox
2. Spread Operator:
Spread operator allows iterables( arrays / objects / strings ) to be expanded into single arguments/elements.
function sum(x, y, z) {
return x + y + z;
}
const numbers = [10, 20, 30];
// Using Apply (ES5)
console.log(sum.apply(null, numbers)); // 60
// Using Spread Operator
console.log(sum(...numbers)); // 60
⚝ Try this example on CodeSandbox
2.1. Copying an array:
const fruits = ["Apple", "Orange", "Banana"];
const newFruitArray = [...fruits];
console.log(newFruitArray);
Output:
['Apple', 'Orange', 'Banana']
⚝ Try this example on CodeSandbox
2.2. Concatenating arrays:
const arr1 = ["A", "B", "C"];
const arr2 = ["X", "Y", "Z"];
const result = [...arr1, ...arr2];
console.log(result);
Output:
['A', 'B', 'C', 'X', 'Y', 'Z']
⚝ Try this example on CodeSandbox
2.3. Spreading elements together with an individual element:
const fruits = ["Apple", "Orange", "Banana"];
const newFruits = ["Cherry", ...fruits];
console.log(newFruits);
Output:
['Cherry', 'Apple', 'Orange', 'Banana']
⚝ Try this example on CodeSandbox
2.4. Spreading elements on function calls:
const fruits = ["Apple", "Orange", "Banana"];
const getFruits = (f1, f2, f3) => {
console.log(`Fruits: ${f1}, ${f2} and ${f3}`);
};
getFruits(...fruits);
Output:
Fruits: Apple, Orange and Banana
⚝ Try this example on CodeSandbox
2.5. Spread syntax for object literals:
const obj1 = { id: 101, name: 'Rajiv Sandal' }
const obj2 = { age: 35, country: 'INDIA' }
const employee = { ...obj1, ...obj2 }
console.log(employee);
Output:
{
"id": 101,
"name": "Rajiv Sandal",
"age": 35,
"country": "INDIA"
}
3. Sets:
Sets are a new object type with ES6 (ES2015) that allow to create collections of unique values. The values in a set can be either simple primitives like strings or integers, but more complex object types like object literals or arrays can also be part of a set.
const numbers = new Set([10, 20, 20, 30, 40, 50]);
console.log(numbers); Set(5) { 10, 20, 30, 40, 50 }
console.log(typeof numbers); // Object
⚝ Try this example on CodeSandbox
4. Default Parametrs:
function add(x = 10, y = 20) {
console.log(x + y);
}
add(10, 30); // Output: 10 + 30 = 40
add(15); // Output: 15 + 20 = 35
add(); // Output: 10 + 20 = 30
⚝ Try this example on CodeSandbox
5. repeat():
The repeat() method constructs and returns a new string which contains the specified number of copies of the string on which it was called, concatenated together.
const msg = "Hello World \n";
console.log(`${msg.repeat(3)}`);
Output:
Hello World
Hello World
Hello World
⚝ Try this example on CodeSandbox
6. Arrow Function (=>):
const add = (x, y) => x + y;
console.log(add(10, 20)); // 30
⚝ Try this example on CodeSandbox
7. Arrow function with this
/**
* Using Arrow function
*/
const person = {
name: "Diksha",
actions: ["bike", "hike", "ski"],
printActions() {
this.actions.forEach((action) => {
console.log(this.name + " likes to " + action);
});
},
};
person.printActions();
Output:
Diksha likes to bike
Diksha likes to hike
Diksha likes to ski
⚝ Try this example on CodeSandbox
8. Destructing Assignment:
// Destructing Assignment
const { title, price, description } = {
title: "iPhone",
price: 999,
description: "The iPhone is a smartphone developed by Apple"
};
console.log(title); // iPhone
console.log(price); // 999
console.log(description); // The iPhone is a smartphone developed by Apple
⚝ Try this example on CodeSandbox
9. Generators:
A generator is a function that can stop midway and then continue from where it stopped. In short, a generator appears to be a function but it behaves like an iterator.
function* generator(num) {
yield num + 10;
yield num + 20;
yield num + 30;
}
const gen = generator(10);
console.log(gen.next().value); // 20
console.log(gen.next().value); // 30
console.log(gen.next().value); // 40
⚝ Try this example on CodeSandbox
10. Symbols:
They are tokens that serve as unique IDs. We create symbols via the factory function Symbol(). Symbols primary use case is for making private object properties, which can be only of type String or Symbol (Numbers are automatically converted to Strings).
const symbol1 = Symbol();
const symbol2 = Symbol(42);
const symbol3 = Symbol("Hi");
console.log(typeof symbol1); // symbol
console.log(symbol3.toString()); // Symbol(Hi)
console.log(Symbol("Hi") === Symbol("Hi")); // false
⚝ Try this example on CodeSandbox
11. Iterator:
The iterable is a interface that specifies that an object can be accessible if it implements a method who is key is [symbol.iterator].
const title = "ES6";
const iterateIt = title[Symbol.iterator]();
console.log(iterateIt.next().value); //output: E
console.log(iterateIt.next().value); //output: S
console.log(iterateIt.next().value); //output: 6
⚝ Try this example on CodeSandbox
Global variables are declared outside of a function or declared with a window object for accessibility throughout the program (unless shadowed by locals). If you declare a variable without using var, even if it's inside a function, it will still be seen as global.
The var statement declares a function-scoped or globally-scoped variable, optionally initializing it to a value.
Example:
var x = 10;
if (x === 10) {
var x = 20;
console.log(x); // expected output: 20
}
console.log(x); // expected output: 20
Example: Declaring global variable within function
window.value = 90;
// Declaring global variable by window object
function setValue() {
window.value = 100;
}
// Accessing global variable from other function
function getValue() {
setValue();
return window.value;
}
console.log(getValue()); // 100
Using Undeclared Variables:
let, const, or var, you'll end up creating a new global variable. It will be global no matter how deeply nested within functions and blocks your code is, which is almost certainly not what you want, is bug-prone, and is one of the best reasons for using strict mode!var: they define properties of the global object. But unlike the properties defined by proper var declarations, these properties can be deleted with the delete operator.⚝ Try this example on CodeSandbox
Template literals help make it simple to do string interpolation, or to include variables in a string.
const person = { name: 'Tyler', age: 28 };
console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`);
// 'Hi, my name is Tyler and I am 28 years old!'
Template literals, however, preserve whatever spacing you add to them. For example, to create that same multi-line output that we created above, you can simply do:
console.log(`This is line one.
This is line two.`);
// This is line one.
// This is line two.
Another use case of template literals would be to use as a substitute for templating libraries for simple variable interpolations:
const person = { name: 'Tyler', age: 28 };
document.body.innerHTML = `
<div>
<p>Name: ${person.name}</p>
<p>Name: ${person.age}</p>
</div>
`
let, var or const?Variables declared using the var keyword are scoped to the function in which they are created, or if created outside of any function, to the global object. let and const are block scoped, meaning they are only accessible within the nearest set of curly braces (function, if-else block, or for-loop).
/**
* All variables are accessible within functions.
*/
function variableScope() {
var x = 10;
let y = 20;
const z = 30;
console.log(x); // 10
console.log(y); // 20
console.log(z); // 30
}
console.log(x); // ReferenceError: x is not defined
console.log(y); // ReferenceError: y is not defined
console.log(z); // ReferenceError: z is not defined
variableScope();
/**
* var declared variables are accessible anywhere in the function scope.
*/
if (true) {
var a = 10;
let b = 20;
const c = 30;
}
console.log(a); // 10
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
var allows variables to be hoisted, meaning they can be referenced in code before they are declared. let and const will not allow this, instead throwing an error.
console.log(a); // undefined
var a = 'foo';
console.log(b); // ReferenceError: can\'t access lexical declaration 'b' before initialization
let b = 'baz';
console.log(c); // ReferenceError: can\'t access lexical declaration 'c' before initialization
const c = 'bar';
Redeclaring a variable with var will not throw an error, but 'let' and 'const' will.
var a = 'foo';
var a = 'bar';
console.log(a); // "bar"
let b = 'baz';
let b = 'qux'; // Uncaught SyntaxError: Identifier 'b' has already been declared
let and const differ in that let allows reassigning the variable's value while const does not.
// This is ok.
let a = 'foo';
a = 'bar';
console.log(a); // bar
// This causes an exception.
const b = 'baz';
b = 'qux';
console.log(b) // TypeError: Assignment to constant variable.
⚝ Try this example on CodeSandbox
The double exclamation mark (!!) in JavaScript forces any value into its corresponding strict boolean representation (true or false). The !! (double negation) operator converts any value to its boolean equivalent:
! negates the value (converting it to a boolean)! negates it again, giving the true boolean representationExample:
console.log(!!0); // false
console.log(!!""); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!!1); // true
console.log(!!"text"); // true
console.log(!!{}); // true
console.log(!![]); // true
Note: It's commonly used to explicitly cast a value to boolean without using Boolean(value).
var x = 1 and x = 1?The primary difference is that var x = 1 explicitly declares a function-scoped or globally-scoped variable, whereas x = 1 assigns a value to an identifier without declaring it, which can lead to global scope pollution or runtime errors depending on your code's strictness mode
var x = 1:
var inside a { } block can be accessed from outside the block.var inside a function are not accessible (visible) from outside the function.var will not trigger an error, even in strict mode, and the variable will not lose its value unless another assignment is performed.Example:
var x = 1;
if (x === 1) {
var x = 2;
console.log(x); // expected output: 2
}
console.log(x); // expected output: 2
var x = 5; // global
function someThing(y) {
var x = 3; // local
var z = x + y;
console.log(z);
}
someThing(4); // 7
console.log(x); // 5
x = 1:
x = 1 is accessible in: (Block scope - Function scope - Global scope)let, const, or var, you'll end up creating a new global variable. It will be global no matter how deeply nested within functions and blocks your code is, which is almost certainly not what you want, is bug-prone, and is one of the best reasons for using strict mode!var they define properties of the global object.var declarations, these properties can be deleted with the delete operator.Example:
var x = 5; // global
function someThing(y) {
x = 1; // still global!
var z = x + y;
console.log(z);
}
someThing(4) // 5
console.log(x) // 1
Example:
{
console.log(x + y); // NaN
var x = 1;
var y = 2;
}
{
console.log(x + y); // Uncaught ReferenceError: x is not defined
x = 1;
y = 2;
}
| var x = 1 | x = 1 | |
|---|---|---|
| Strict mode | ✔ | ❌ |
| Block scope | ❌ | ✔ |
| Function scope | ✔ | ✔ |
| Global scope | ✔ | ✔ |
| Hoisting | ✔ | ❌ |
| Reassigning | ✔ | ✔ |
In JavaScript, you can assign default values to variables using several modern and legacy approaches, depending on the exact conditions you want to check.
1. OR operator (||)
let name = userInput || "Default";
// Uses "Default" if userInput is falsy (0, "", null, undefined, false)
2. Nullish coalescing (??) (preferred)
let name = userInput ?? "Default";
// Uses "Default" only if userInput is null or undefined (not 0 or "")
3. Destructuring defaults
const { name = "John", age = 25 } = user;
const [first = 0, second = 0] = arr;
4. Function parameter defaults
function greet(name = "World") {
return `Hello, ${name}!`;
}
greet(); // "Hello, World!"
greet("Alice"); // "Hello, Alice!"
5. Logical OR assignment (||=)
let x = null;
x ||= "default"; // x = "default"
6. Nullish assignment (??=)
let x = 0;
x ??= "default"; // x stays 0 (not null/undefined)
Key difference between || and ??:
let count = 0;
count || 10; // 10 — 0 is falsy, so falls back
count ?? 10; // 0 — 0 is not null/undefined, so keeps it
Note: Use ?? when 0, "", or false are valid values.
Local variables always take precedence over global variables within their scope.
let x = "global";
function example() {
let x = "local";
console.log(x); // "local" — local shadows the global
}
example();
console.log(x); // "global" — unchanged
Scope chain — JavaScript looks up variables from innermost to outermost:
let x = "global";
function outer() {
let x = "outer";
function inner() {
let x = "inner";
console.log(x); // "inner"
}
inner();
console.log(x); // "outer"
}
outer();
console.log(x); // "global"
If no local variable exists, it walks up the chain:
let x = "global";
function example() {
console.log(x); // "global" — no local x, found in outer scope
}
Shadowing pitfall:
let x = "global";
function example() {
console.log(x); // ReferenceError (TDZ) — let/const are hoisted but not initialized
let x = "local";
}
Precedence order (innermost → outermost):
window / globalThis)⚝ Try this example on CodeSandbox
Variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope. This outer variable is said to be shadowed.
If there is a variable in the global scope, and you'd like to create a variable with the same name in a function. The variable in the inner scope will temporarily shadow the variable in the outer scope.
Example:
let x = "global";
function example() {
let x = "local"; // shadows the global x
console.log(x); // "local"
}
example();
console.log(x); // "global" — outer x is unaffected
Block-level shadowing (let/const):
let msg = "outside";
if (true) {
let msg = "inside"; // shadows outer msg
console.log(msg); // "inside"
}
console.log(msg); // "outside"
var does NOT shadow at block level (only at function level):
var msg = "outside";
if (true) {
var msg = "inside"; // overwrites, not shadows!
console.log(msg); // "inside"
}
console.log(msg); // "inside" — original was overwritten!
Nested function shadowing:
let value = 1;
function outer() {
let value = 2; // shadows global
function inner() {
let value = 3; // shadows outer
console.log(value); // 3
}
inner();
console.log(value); // 2
}
outer();
console.log(value); // 1
Note:
let/const shadow at block level; var only at function levelThe scope chain is the mechanism JavaScript uses to resolve variable references by searching through nested scopes from innermost to outermost until the variable is found (or a ReferenceError is thrown).
Example::
let globalVar = "I'm a global variable";
function outer() {
let outerVar = "I'm an outer variable";
function inner() {
let innerVar = "I'm an inner variable";
console.log(innerVar); // "I'm an inner variable"
console.log(outerVar); // "I'm an outer variable"
console.log(globalVar); // "I'm a global variable"
}
inner();
}
outer();
Note:
There are eight basic data types in JavaScript.
| Data Types | Description | Example |
|---|---|---|
| String | Represents textual data | let str = 'Hi', let str2 = "Hello", let str3 = `Hello World` |
| Number | An integer or a floating-point number | let num = 3, let num2 = 3.234, let num3 = 3e-2 |
| BigInt | An integer with arbitrary precision | let num = 900719925124740999n, let num = 1n |
| Boolean | Any of two values: true or false | let flag = true |
| undefined | A data type whose variable is not initialized | let a; |
| null | Denotes a null value | let a = null; |
| Symbol | Data type whose instances are unique and immutable | let value = Symbol('hello'); |
| Object | key-value pairs of collection of data | let student = { }; |
String:
String is used to store text. In JavaScript, strings are surrounded by quotes:
Example::
// Strings
const firstName = "Mukul";
const lastName = "Mittal";
const result = `Name: ${firstName} ${lastName}`;
console.log(result); // Name: Mukul Mittal
console.log(typeof result); // string
Number:
Number represents integer and floating numbers (decimals and exponentials). A number type can also be +Infinity, -Infinity, and NaN (not a number).
const number1 = 3;
const number2 = 3.433;
const number3 = 3e5; // 3 * 10^5
const number4 = 3 / 0;
console.log(number4); // Infinity
console.log(typeof number4) // number
const number5 = -3 / 0;
console.log(number5); // -Infinity
console.log(typeof number5); // number
// strings can\'t be divided by numbers
const number6 = "abc" / 3;
console.log(number6); // NaN
console.log(typeof number6); // number
BigInt:
In JavaScript, Number type can only represent numbers less than (253 - 1) and more than -(253 - 1). However, if you need to use a larger number than that, you can use the BigInt data type.
A BigInt number is created by appending n to the end of an integer.
// BigInt value
const num1 = 100000000000000000n;
const num2 = 1000000000000000000n;
const num3 = 10;
// Adding two big integers
const result1 = num1 + num2;
console.log(result1); // "1100000000000000000n"
console.log(typeof result1); // bigint
// Error! BitInt and number cannot be added
const result2 = num1 + num2 + num3;
console.log(result2); // Uncaught TypeError: Cannot mix BigInt and other types
console.log(typeof result2); // Uncaught TypeError: Cannot mix BigInt and other types
Boolean:
This data type represents logical entities. Boolean represents one of two values: true or false.
const dataChecked = true;
const valueCounted = false;
undefined:
The undefined data type represents value that is not assigned. If a variable is declared but the value is not assigned, then the value of that variable will be undefined.
let name;
console.log(name); // undefined
let name = undefined;
console.log(name); // undefined
null:
In JavaScript, null is a special value that represents empty or unknown value.
const number = null;
Symbol:
A value having the data type Symbol can be referred to as a symbol value. Symbol is an immutable primitive value that is unique.
// Two symbols with the same description
const value1 = Symbol('hello');
const value2 = Symbol('hello');
let result = (value1 === value2) ? true : false; // false;
// Note: Though value1 and value2 both contain 'hello', they are different as they are of the Symbol type.
Object:
An object is a complex data type that allows us to store collections of data.
const employee = {
firstName: 'John',
lastName: 'K',
email: 'john.k@gmail.com'
};
undefined property?The undefined property indicates that a variable has not been assigned a value, or not declared at all. The type of undefined value is undefined too.
var user; // Value is undefined, type is undefined
console.log(typeof(user)) //undefined
Any variable can be emptied by setting the value to undefined.
user = undefined
null vs undefined?Null:
Null means an empty or non-existent value. Null is assigned, and explicitly means nothing.
var test = null;
console.log(test); // null
null is also an object. Interestingly, this was actually an error in the original JavaScript implementation:
console.log(typeof test); // object
Undefined:
Undefined means a variable has been declared, but the value of that variable has not yet been defined. For example:
var test2;
console.log(test2); // undefined
Unlike null, undefined is of the type undefined:
console.log(typeof test2); // undefined
Difference:
| Null | Undefined |
|---|---|
| It is an assignment value which indicates that variable points to no object. | It is not an assignment value where a variable has been declared but has not yet been assigned a value. |
| Type of null is object | Type of undefined is undefined |
| The null value is a primitive value that represents the null, empty, or non-existent reference. | The undefined value is a primitive value used when a variable has not been assigned a value. |
| Indicates the absence of a value for a variable | Indicates absence of variable itself |
| Converted to zero (0) while performing primitive operations | Converted to NaN while performing primitive operations |
Type coercion is the automatic or implicit conversion of values from one data type to another (such as strings to numbers). Type conversion is similar to type coercion because they both convert values from one data type to another with one key difference — type coercion is implicit whereas type conversion can be either implicit or explicit.
Example:
const value1 = '10';
const value2 = 20;
let sum = value1 + value2;
console.log(sum);
In the above example, JavaScript has coerced the 10 from a number into a string and then concatenated the two values together, resulting in a string of 1020. JavaScript had a choice between a string or a number and decided to use a string.
// Example of explicit coercion
const value1 = '10';
const value2 = 20;
let sum = Number(value1) + value2;
console.log(sum);
An operator is capable of manipulating (mathematical and logical computations) a certain value or operand. There are various operators supported by JavaScript as below,
Arithmetic Operators:
Arithmetic operators are used to perform mathematical operations between numeric operands.
| Operators | Description | Example ( say let x = 10, y = 20;) |
|---|---|---|
| + | Adds two numeric operands. | x + y |
| - | Subtract right operand from left operand | y - x |
| * | Multiply two numeric operands. | x * y |
| / | Divide left operand by right operand. | y / x |
| % | Modulus operator. Returns remainder of two operands. | x % 2 |
| ++ | Increment operator. Increase operand value by one. | x++ |
| -- | Decrement operator. Decrease value by one. | x-- |
Comparison Operators:
JavaScript provides comparison operators that compare two operands and return a boolean value true or false.
| Operators | Description |
|---|---|
| == | Compares the equality of two operands without considering type. |
| === | Compares equality of two operands with type. |
| != | Compares inequality of two operands. |
| > | Returns a boolean value true if the left-side value is greater than the right-side value; otherwise, returns false. |
| < | Returns a boolean value true if the left-side value is less than the right-side value; otherwise, returns false. |
| >= | Returns a boolean value true if the left-side value is greater than or equal to the right-side value; otherwise, returns false. |
| <= | Returns a boolean value true if the left-side value is less than or equal to the right-side value; otherwise, returns false. |
Logical Operators:
The logical operators are used to combine two or more conditions.
1. && - is known as AND operator. It checks whether two operands are non-zero or not (0, false, undefined, null or "" are considered as zero). It returns 1 if they are non-zero; otherwise, returns 0
2. || - is known as OR operator. It checks whether any one of the two operands is non-zero or not (0, false, undefined, null or "" is considered as zero). It returns 1 if any one of of them is non-zero; otherwise, returns 0.
3. ! - is known as NOT operator. It reverses the boolean result of the operand (or condition). !false returns true, and !true returns false.
Assignment Operators:
The assignment operators to assign values to variables with less key strokes.
| Operators | Description |
|---|---|
| = | Assigns right operand value to the left operand. |
| += | Sums up left and right operand values and assigns the result to the left operand. |
| -= | Subtract right operand value from the left operand value and assigns the result to the left operand. |
| *= | Multiply left and right operand values and assigns the result to the left operand. |
| /= | Divide left operand value by right operand value and assign the result to the left operand. |
| %= | Get the modulus of left operand divide by right operand and assign resulted modulus to the left operand. |
Below are the list of bit-wise logical operators used in JavaScript
| Operator | Usage | Description |
|---|---|---|
| Bitwise AND | a & b | Returns a one in each bit position for which the corresponding bits of both operands are ones. |
| Bitwise OR | a | b |
| Bitwise XOR | a ^ b | Returns a one in each bit position for which the corresponding bits of either but not both operands are ones. |
| Bitwise NOT | ~ a | Inverts the bits of its operand. |
| Left shift | a << b | Shifts a in binary representation b (< 32) bits to the left, shifting in zeroes from the right. |
| Sign-propagating right shift | a >> b | Shifts a in binary representation b (< 32) bits to the right, discarding bits shifted off. |
| Zero-fill right shift | a >>> b | Shifts a in binary representation b (< 32) bits to the right, discarding bits shifted off, and shifting in zeroes from the left. |
Examples:
| Operation | Result | Same as | Result |
|---|---|---|---|
| 5 & 1 | 1 | 0101 & 0001 | 0001 |
| `5 | 1` | 5 | 0101 |
~ 5 | -6 | ~0101 | 1010 |
5 << 1 | 10 | 0101 << 1 | 1010 |
5 ^ 1 | 4 | 0101 ^ 0001 | 0100 |
5 >> 1 | 2 | 0101 >> 1 | 0010 |
| 5 >>> 1 | 2 | 0101 >>> 1 | 0010 |
⚝ Try this example on CodeSandbox
== and === operators?JavaScript provides both strict (===, !==) and type-converting (==, !=) equality comparison. The strict operators takes type of variable in consideration, while non-strict operators make type correction/conversion based upon values of variables. The strict operators follow the below conditions for different types,
===, but equal with ==. i.e,
null === undefined --> false but null == undefined --> trueExample:
0 == false // true
0 === false // false
1 == "1" // true
1 === "1" // false
null == undefined // true
null === undefined // false
"0" == false // true
"0" === false // false
[] === [] // false, refer different objects in memory
{} === {} // false, refer different objects in memory
⚝ Try this example on CodeSandbox
In JavaScript, the typeof operator returns the data type of its operand in the form of a string. The operand can be any object, function, or variable.
Example:
console.log(typeof undeclaredVariable); // "undefined"
let a;
console.log(typeof a); // "undefined"
const b = "Hello World";
console.log(typeof b); // "string"
const c = 42;
console.log(typeof c); // "number"
const d = 3.1415;
console.log(typeof d); // "number"
const e = true;
console.log(typeof e); // "boolean"
const f = null;
console.log(typeof f); // "object"
const g = undefined;
console.log(typeof g); // "undefined"
const h = { b: "c" };
console.log(typeof h); // "object"
const i = function () {
return 10;
};
console.log(typeof i); // "function"
⚝ Try this example on CodeSandbox
The unary(+) operator is used to convert a variable to a number. If the variable cannot be converted, it will still become a number but with the value NaN.
Example:
var x = "100";
var y = +x;
console.log(typeof x, typeof y); // string, number
var a = "Hello";
var b = +a;
console.log(typeof a, typeof b, b); // string, number, NaN
⚝ Try this example on CodeSandbox
The delete keyword is used to delete the property as well as its value.
Example:
const user = { name: "Sadhika Chaudhuri", age: 24 };
delete user.age;
console.log(user); // { name: "Sadhika Chaudhuri" }
The conditional (ternary) operator is the only JavaScript operator that takes three operands which acts as a shortcut for if statement.
Syntax:
<condition> ? <value1> : <value2>;
Example:
const isAuthenticated = false;
console.log(isAuthenticated ? 'Hello, welcome' : 'Sorry, you are not authenticated');
Yes, you can apply chaining on conditional operator similar to if … else if … else if … else chain.
Example:
function getValue(someParam) {
return condition1 ? value1
: condition2 ? value2
: condition3 ? value3
: value4;
}
// The above conditional operator is equivalent to:
function getValue(someParam) {
if (condition1) {
return value1;
} else if (condition2) {
return value2;
} else if (condition3) {
return value3;
} else {
return value4;
}
}
typeof and instanceof operator?The typeof operator checks if a value has type of primitive type which can be one of boolean, function, object, number, string, undefined, symbol, or bigint.
Example:
const x = "Hello World";
const y = new String("Hello World");
typeof x; // returns 'string'
typeof y; // returns 'object'
The instanceof is a binary operator, accepting an object and a constructor. It returns a boolean indicating whether or not the object has the given constructor in its prototype chain.
const a = "Hello World";
const b = new String("Hello World");
a instanceof String; // returns false
b instanceof String; // returns true
⚝ Try this example on CodeSandbox
[...'Hello']
Output: ['H', 'e', 'l', 'l', 'o']
Explanation: The string is an iterable type and the spread operator with in an array maps every character of an iterable to one element. Hence, each character of a string becomes an element within an Array.
??) in JavaScript?The nullish coalescing operator (??) returns the right-hand side operand when the left-hand side operand is null or undefined. Otherwise, it returns the left-hand side operand. Unlike ||, it does not treat 0, false, or "" as falsy.
Syntax:
leftExpr ?? rightExpr
Example:
console.log(null ?? 'default'); // 'default'
console.log(undefined ?? 'default'); // 'default'
console.log(0 ?? 'default'); // 0 (0 is NOT null/undefined)
console.log('' ?? 'default'); // '' (empty string is NOT null/undefined)
console.log(false ?? 'default'); // false
// Practical use: provide a fallback for a missing config value
const config = { timeout: 0 };
const timeout = config.timeout ?? 3000;
console.log(timeout); // 0 (not 3000, because 0 is a valid value)
Nullish coalescing assignment (??=):
let user = { name: null };
user.name ??= 'Anonymous';
console.log(user.name); // 'Anonymous'
Logical assignment operators (ES2021) combine a logical operator with assignment. There are three:
| Operator | Equivalent to | Description |
|---|---|---|
&&= | a && (a = b) | Assigns b to a only if a is truthy |
||= | a || (a = b) | Assigns b to a only if a is falsy |
??= | a ?? (a = b) | Assigns b to a only if a is null or undefined |
Example:
// &&= (AND assignment)
let a = 1;
a &&= 2;
console.log(a); // 2 (a was truthy, so assigned)
let b = 0;
b &&= 2;
console.log(b); // 0 (b was falsy, not assigned)
// ||= (OR assignment)
let c = null;
c ||= 'default';
console.log(c); // 'default' (c was falsy)
let d = 'existing';
d ||= 'default';
console.log(d); // 'existing' (d was truthy, not reassigned)
// ??= (Nullish assignment)
let e = null;
e ??= 'fallback';
console.log(e); // 'fallback'
let f = 0;
f ??= 'fallback';
console.log(f); // 0 (0 is not null/undefined)
The Math.random() function returns a floating-point, pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1). For example, if you want generate random integers between 1 to 100, the multiplication factor should be 100,
// Example 01:
Math.random(); // returns a random integer between 0 to 1
// Example 02:
Math.floor(Math.random() * 100) + 1; // returns a random integer from 1 to 100
// Example 03:
function getRandomNumber(max) {
return Math.floor(Math.random() * max) + 1;
}
console.log(getRandomNumber(10)); // returns a random integer from 1 to 10
Random integer between min and max (inclusive):
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomInt(1, 6)); // simulates a dice roll: 1–6
⚝ Try this example on CodeSandbox
The isNaN() function determines whether a value is NaN ( Not a Number ) or not. This function returns true if the value equates to NaN. The isNaN() method converts the value to a number before testing it.
Example:
isNaN('Hello') // true
isNaN('100') // false
typeof NaN // Number
Number.isNaN('Hello'); // false
⚝ Try this example on CodeSandbox
The global isFinite() function determines whether the passed value is a finite number. It returns false if the value is +infinity, -infinity, or NaN (Not-a-Number), otherwise it returns true.
isFinite(Infinity); // false
isFinite(NaN); // false
isFinite(-Infinity); // false
isFinite(100); // true
isFinite(1/0); // false
Number.isFinite(0 / 0); // false
Number.isFinite(null); // false
Number.isFinite("123") // false
⚝ Try this example on CodeSandbox
The Number.NEGATIVE_INFINITY property represents the negative Infinity value.
Syntax:
Number.NEGATIVE_INFINITY
/**
* NEGATIVE_INFINITY
*/
console.log(-10/0); // -Infinity
console.log(Number.NEGATIVE_INFINITY); // -Infinity
console.log(Number.MAX_VALUE + Number.MAX_VALUE); // Infinity
console.log(-2 * Number.MAX_VALUE); // -Infinity
console.log("Math.pow(10, 1000): " + Math.pow(10, 1000)); // Infinity
console.log("Math.log(0): " + Math.log(0)); // -Infinity
console.log(Number.NEGATIVE_INFINITY === -2 * Number.MAX_VALUE); // true
⚝ Try this example on CodeSandbox
1. slice():
The slice() method returns a new array with a copied slice from the original array. The first optional argument is the beginning index and the second optional argument is the ending index (non-inclusive).
Example:
let languages = [ "JavaScript", "Python", "Java", "PHP" ];
languages.slice(1,3); // ["Python", "Java"]
languages.slice(2); // (from index 2 until the end of the array).
// ["Java", "PHP"]
console.log(languages); // the original array is not mutated.
// [ "JavaScript", "Python", "Java", "PHP" ]
2. splice():
The splice() method changes the content of the array in place and can be used to add or remove items from the array.
When only one argument is provided, all the items after the provided starting index are removed from the array.
Example:
let numbers = [10, 20, 30];
numbers.splice(2, 1, 40, 50); // returns removed array:[30]
console.log(numbers); // Original array is mutated.
// returns: [10, 20, 40, 50]
Difference:
| Slice | Splice |
|---|---|
| Doesn't modify the original array(immutable) | Modifies the original array(mutable) |
| Returns the subset of original array | Returns the deleted elements as array |
| Used to pick the elements from array | Used to insert or delete elements to/from array |
⚝ Try this example on CodeSandbox
There are 3 fastest ways to check whether a string contains a substring or not,
1. Using RegEx:
The regular expression test() method checks if a match exists in a string. This method returns true if it finds a match, otherwise, it returns false.
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
let exp1 = /MongoDB/g;
let exp2 = /Ajax/;
exp1.test(str); // true
exp2.test(str); // false
2. Using indexOf:
The indexOf() method is case-sensitive and accepts two parameters. The first parameter is the substring to search for, and the second optional parameter is the index to start the search from (default index is 0).
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
str.indexOf('MongoDB') !== -1 // true
str.indexOf('PHP') !== -1 // false
str.indexOf('Node', 5) !== -1 // true
3. Using includes: (Modern Standard)
The includes() is also case-sensitive and accepts an optional second parameter, an integer which indicates the position where to start searching for.
let str = "JavaScript, Node.js, Express.js, React.js, MongoDB";
str.includes('MongoDB') // true
str.includes('PHP') // false
str.includes('Node', 5) //true
Use Cases:
| Use Case | Method | Example |
|---|---|---|
| Simple readability check | includes() | Check if a product description contains a keyword |
| Legacy browser support | indexOf() | Check if URL contains a query param |
| Pattern-based search | RegEx test() | Check if input matches a specific format |
// 1. Search bar — check if item name contains user query
const query = "react";
const items = ["React.js Tutorial", "Node.js Guide", "React Native Basics"];
const results = items.filter(item => item.toLowerCase().includes(query.toLowerCase()));
// ["React.js Tutorial", "React Native Basics"]
// 2. URL parsing — check if a query param exists
const url = "https://example.com/search?q=javascript&page=2";
const hasQuery = url.indexOf('?q=') !== -1; // true
// 3. Input validation — check if email contains '@' and '.'
const email = "user@example.com";
const isValidEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); // true
⚝ Try this example on CodeSandbox
The trim() method removes whitespace from both sides of a string. JavaScript provides 3 simple functions on how to trim strings.
1. string.trim():
The string.trim() removes sequences of whitespaces and line terminators from both the start and the end of the string.
const name = " Karan Talwar ";
console.log(name.trim()); // => 'Karan Talwar'
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trim()); // => '80-555-123'
2. string.trimStart():
The string.trimStart() removes sequences of whitespaces and line terminators only from the start of the string.
const name = " Karan Talwar ";
console.log(name.trimStart()); // => "Karan Talwar "
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trimStart()); // => "80-555-123 "
3. string.trimEnd():
The string.trimEnd() removes sequences of whitespaces and line terminators only from the end of the string.
const name = " Karan Talwar ";
console.log(name.trimEnd()); // => " Karan Talwar"
const phoneNumber = "\t 80-555-123\n ";
console.log(phoneNumber.trimEnd()); // => " 80-555-123"
⚝ Try this example on CodeSandbox
The eval() function evaluates JavaScript code represented as a string. The string can be a JavaScript expression, variable, statement, or sequence of statements.
console.log(eval('10 + 20')); // 30
let x = 10;
let y = 20;
let z = '50';
eval('x + y + 1'); // returns 31
eval(z); // returns 50
If the argument of eval() is not a string, eval() returns the argument unchanged. In the following example, the String constructor is specified and eval() returns a String object rather than evaluating the string.
eval(new String('10 + 20')); // returns a String object containing "10 + 20"
eval('10 + 20'); // returns 30
// work around
let expression = new String('10 + 20');
eval(expression.toString()); // returns 30
Warning: Executing JavaScript from a string is an enormous security risk. It is far too easy for a bad actor to run arbitrary code when you use eval().
⚝ Try this example on CodeSandbox
You can use String.prototype.startsWith() method to check if a string starts with another string or not. It is fully supported in all modern browsers.
let str = "Hello World";
console.log(str.startsWith("Hello")); // true
console.log(str.startsWith("World")); // false
⚝ Try this example on CodeSandbox
replaceAll(), padStart() and padEnd() string methods?1. String.prototype.replaceAll():
The replaceAll() method returns a new string with all matches of a pattern replaced by a replacement. Unlike replace(), it replaces every occurrence without needing a global regex flag.
const text = 'I like cats. Cats are cute cats.';
console.log(text.replace('cats', 'dogs')); // 'I like dogs. Cats are cute cats.' (only first)
console.log(text.replaceAll('cats', 'dogs')); // 'I like dogs. Cats are cute dogs.'
2. String.prototype.padStart():
The padStart() method pads the current string with another string from the start until it reaches the given length.
console.log('5'.padStart(3, '0')); // '005'
console.log('hello'.padStart(8)); // ' hello' (default pad is space)
console.log('42'.padStart(5, '*')); // '***42'
3. String.prototype.padEnd():
The padEnd() method pads the current string with another string from the end until it reaches the given length.
console.log('5'.padEnd(3, '0')); // '500'
console.log('hello'.padEnd(8)); // 'hello '
console.log('42'.padEnd(5, '-')); // '42---'
JavaScript array is an object that represents a collection of similar type of elements. It can holds values (of any type) not particularly in named properties/keys, but rather in numerically indexed positions.
Syntax:
const array_name = [item-1, item-2, item-3, ...];
Example 01: Creating an array
// array of numbers
const numbers = [10, 20, 30, 40, 50];
// using new keyword
const numbers = new Array(10, 20, 30, 40, 50);
// array of strings
let fruits = ["Apple", "Orange", "Plum", "Mango"];
// array of character
const numbers = Array.from("abc"); // ["a", "b", "c"]
// array of numbers
const numbers = Array.of(10, 20, 30); // [10, 20, 30]
Common Methods
| Category | Methods |
|---|---|
| Add/Remove | push, pop, shift, unshift, splice |
| Extract | slice, flat, flatMap |
| Search | find, findIndex, indexOf, includes |
| Iterate | forEach, map, filter, reduce |
| Sort/Order | sort, reverse |
| Combine | concat, join |
Example 02: Accessing array elements
const numbers = [10, 20, 30, 40, 50];
// Transforming
console.log(numbers.map(n => n * 2)); // [20, 40, 60, 80, 100]
// Filtering
console.log(numbers.filter(n => n % 2 === 0)); // [10, 20, 30, 40, 50]
// Reducing
console.log(numbers.reduce((sum, n) => sum + n, 0)); // 150
// Searching
console.log(numbers.find(n => n > 30)); // 40
console.log(numbers.includes(30)); // true
Example 03: Adding new array elements
let fruits = ["Apple", "Orange", "Plum", "Mango"];
fruits.push("Grapes"); // Adds a new element (Grapes) to fruits
⚝ Try this example on CodeSandbox
JavaScript does not have built-in associative arrays in the way languages like PHP or literal dictionary structures do. Instead, JavaScript uses regular Objects or Maps to handle key-value pairs (associative behavior).
Associative arrays are basically objects in JavaScript where indexes are replaced by user-defined keys. They do not have a length property like a normal array and cannot be traversed using a normal for loop.
Syntax:
const array_name = { key1: 'value1', key2: 'value2', key3: 'value3' }
Example: Plain Object (most common)
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
// Accessing employee elements
console.log(employee.id); // 12345
console.log(employee.name); // Sakshi Memon
// Array Length
console.log(Object.keys(employee).length); // 3
// Retrieve the elements
for (let key in employee) {
console.log(key + " = " + employee[key]);
}
// Output
id = 12345
name = Sakshi Memon
email = sakshi.memon@email.com
Example: Map (true associative structure)
const map = new Map();
map.set("name", "Alice");
map.set(42, "answer"); // keys can be any type
map.set({ id: 1 }, "object key");
map.get("name"); // "Alice"
map.has(42); // true
map.size; // 3
⚝ Try this example on CodeSandbox
Method 1: Using Object.keys().length
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
console.log(Object.keys(employee).length); // 3
Method 2: Using Object.hasOwnProperty()
function getLength(object) {
let count = 0;
for (let key in object) {
// hasOwnProperty method check own property of object
if (object.hasOwnProperty(key)) count++;
}
return count;
}
console.log(getLength(employee)); // 3
Method 3: Using Object.getOwnPropertyNames()
const employee = {
id: 12345,
name: "Sakshi Memon",
email: "sakshi.memon@email.com"
};
Object.getOwnPropertyNames(employee).length; // 3
⚝ Try this example on CodeSandbox
Objects represent a special data type that is mutable and can be used to store a collection of data (rather than just a single value). Arrays are a special type of variable that is also mutable and can also be used to store a list of values.
Example: Arrays
const numbers = [10, 20, 30];
// Iterating through loop
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
// Pop an element from array
numbers.pop();
console.log("after pop(): " + numbers);
Example: Array of Objects
const employees = [
{ id: 101, name: "Sakshi Memon", email: "sakshi.memon@email.com" },
{ id: 102, name: "Subhash Shukla", email: "subhash.shukla@email.com" },
{ id: 103, name: "Mohini Karpe", email: "mohini.karpe@email.com" }
];
// Using DOT notation
console.log(employees[0].name);
// Using delete keyword
delete employees[0];
// Iterating using for..in loop
for (let key in employees) {
console.log(employees[key]);
}
Difference:
| S.No. | Array | Array of objects |
|---|---|---|
| 1. | Arrays are best to use when the elements are numbers. | Objects are best to use when the elements strings |
| 3. | The elements can be manipulated using []. | The properties can be manipulated using both . ( DOT ) notation and []. |
| 4. | The elements can be popped out of an array using the pop() function. | The keys or properties can be deleted by using the delete keyword. |
| 5. | Iterating through an array is possible using For loop, For..in, For..of, and ForEach(). | Iterating through an array of objects is possible using For..in, For..of, and ForEach(). |
⚝ Try this example on CodeSandbox
1. array.join():
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
var elements = ['Fire', 'Air', 'Water'];
console.log(elements.join()); // Output: "Fire,Air,Water"
console.log(elements.join('')); // Output: "FireAirWater"
console.log(elements.join('-')); // Output: "Fire-Air-Water"
2. array.pop():
The pop() method removes the last element from an array and returns that element. This method changes the length of the array.
var plants = ['broccoli', 'cauliflower', 'kale'];
console.log(plants.pop()); // Output: "kale"
console.log(plants); // Output: Array ["broccoli", "cauliflower"]
console.log(plants.pop()); // Output: "cauliflower"
console.log(plants.pop()); // Output: "broccoli"
console.log(plants.pop()); // Output: "undefined"
3. array.push():
The push() method adds one or more elements to the end of an array and returns the new length of the array.
const animals = ['pigs', 'goats', 'sheep'];
const count = animals.push('cows');
console.log(count); // Output: 4
console.log(animals); // Output: Array ["pigs", "goats", "sheep", "cows"]
4. array.shift():
The shift() method removes the first element from an array and returns that removed element. This method changes the length of the array.
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.shift();
console.log(fruits) // Output: Array ["Orange", "Apple", "Mango"]
5. array.unshift():
The unshift() method adds one or more elements to the beginning of an array and returns the new length of the array.
var fruits = ["Banana", "Orange", "Apple"];
fruits.unshift("Mango","Pineapple");
console.log(fruits); // Output: Array ["Mango", "Pineapple", "Banana", "Orange", "Apple"]
6. array.concat():
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
console.log(array1.concat(array2)); // Output: Array ["a", "b", "c", "d", "e", "f"]
7. array.map():
The map() method creates a new array with the results of calling a provided function on every element in the calling array.
var array1 = [1, 4, 9, 16];
// pass a function to map
const map1 = array1.map(x => x * 2);
console.log(map1); // Output: Array [2, 8, 18, 32]
8. array.filter():
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
var words = ['spray', 'limit', 'elite', 'exuberant', 'destruction'];
const result = words.filter(word => word.length > 6);
console.log(result); // Output: Array ["exuberant", "destruction"]
9. array.reduce():
The reduce() method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.
const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;
console.log(array1.reduce(reducer)); // Output: 10
console.log(array1.reduce(reducer, 5)); // Output: 15
10. array.reduceRight():
The reduceRight() method applies a function against an accumulator and each value of the array (from right-to-left) to reduce it to a single value.
const array1 = [[0, 1], [2, 3], [4, 5]].reduceRight(
(accumulator, currentValue) => accumulator.concat(currentValue)
);
console.log(array1); // Output: Array [4, 5, 2, 3, 0, 1]
11. array.every():
The every() method tests whether all elements in the array pass the test implemented by the provided function. It returns a Boolean value.
function isBelowThreshold(currentValue) {
return currentValue < 40;
}
var array1 = [1, 30, 39, 29, 10, 13];
console.log(array1.every(isBelowThreshold)); // Output: true
12. array.some():
The some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value.
var array = [1, 2, 3, 4, 5];
var even = function(element) {
// checks whether an element is even
return element % 2 === 0;
};
console.log(array.some(even)); // Output: true
13. array.indexOf():
The indexOf() method returns the first index at which a given element can be found in the array, or -1 if it is not present.
var beasts = ['ant', 'bison', 'camel'];
console.log(beasts.indexOf('camel')); // Output: 2
console.log(beasts.indexOf('giraffe')); // Output: -1
14. array.lastIndexOf():
The lastIndexOf() method returns the index within the calling String object of the last occurrence of the specified value, searching backwards from fromIndex. Returns -1 if the value is not found.
var paragraph = 'The quick brown fox jumps over the lazy dog. If the dog barked, was it really lazy?';
var searchTerm = 'dog';
console.log('The index of the first "' + searchTerm + '" from the end is ' + paragraph.lastIndexOf(searchTerm));
// Output: "The index of the first "dog" from the end is 52"
15. array.find():
The find() method returns the value of the first element in the provided array that satisfies the provided testing function.
var array1 = [5, 12, 8, 130, 44];
var found = array1.find(function(element) {
return element > 100;
});
console.log(found); // Output: 130
16. array.findIndex():
The findIndex() method returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test.
var array1 = [5, 12, 8, 130, 44];
function isLargeNumber(element) {
return element > 20;
}
console.log(array1.findIndex(isLargeNumber)); // Output: 3
17. array.includes():
The includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate.
var array1 = [1, 2, 3];
console.log(array1.includes(2)); // Output: true
var pets = ['cat', 'dog', 'bat'];
console.log(pets.includes('at')); // Output: false
18. array.at():
The at() method (ES2022) takes an integer and returns the item at that index. Negative integers count back from the last item.
const fruits = ['Apple', 'Orange', 'Banana', 'Mango'];
console.log(fruits.at(0)); // Output: Apple
console.log(fruits.at(-1)); // Output: Mango (last element)
console.log(fruits.at(-2)); // Output: Banana
19. array.flat():
The flat() method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth.
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat()); // Output: [1, 2, 3, 4, [5, 6]]
console.log(nested.flat(2)); // Output: [1, 2, 3, 4, 5, 6]
console.log(nested.flat(Infinity)); // Output: [1, 2, 3, 4, 5, 6]
20. array.flatMap():
The flatMap() method maps each element using a mapping function, then flattens the result into a new array (one level deep).
const sentences = ['Hello World', 'Foo Bar'];
console.log(sentences.flatMap(s => s.split(' '))); // Output: ['Hello', 'World', 'Foo', 'Bar']
21. array.findLast():
The findLast() method (ES2023) iterates the array in reverse and returns the value of the first element that satisfies the provided testing function.
const numbers = [5, 12, 8, 130, 44];
console.log(numbers.findLast(n => n > 10)); // Output: 44
22. array.findLastIndex():
The findLastIndex() method (ES2023) iterates the array in reverse and returns the index of the first element that satisfies the provided testing function.
const numbers = [5, 12, 8, 130, 44];
console.log(numbers.findLastIndex(n => n > 10)); // Output: 4
Spread operator or Spread Syntax allow us to expand the arrays and objects into elements in the case of an array and key-value pairs in the case of an object.
Example:
function sum(x, y, z) {
return x + y + z;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers));
// ES-5 way
console.log(sum.apply(null, numbers));
Example: Merge arrays
const newBrands = ["Tesla", "Mahindra"];
const brands = ["Ford", "Honda", ...newBrands, "BMW"];
console.log(brands);
Example: Copy array/object
let obj = { a: 10, b: 20, c: 30 };
// spread the object into a list of parameters
let objCopy = { ...obj };
// add new
obj.d = 40;
console.log(JSON.stringify(obj)); // { "a":10, "b":20, "c":30, "d":40 }
console.log(JSON.stringify(objCopy)); // { "a":10, "b":20, "c":30 }
Difference:
The main difference between rest and spread is that the rest operator puts the rest of some specific user-supplied values into a JavaScript array. But the spread syntax expands iterables into individual elements.
| Spread Syntax | Rest Syntax |
|---|---|
| Spread operator as its name suggests it spreads or expands the content of the given element. | Rest Syntax is just the opposite of spread syntax it collects the data and stores that data in a variable which we can use further in our code. |
| It expands an Array in form of elements, while in key-value pairs in the case of Objects. | It collects the data in the developer's desired format. |
| You may or may not use the strict mode inside the function containing the spread operator. | You can not use the strict mode inside function containing the rest operator. |
| It will overwrite the identical properties inside two objects and replace the former with the latter. | It simply collects all properties and wraps them inside a container. |
⚝ Try this example on CodeSandbox
Both for..of and for..in statements iterate over lists; the values iterated on are different though, for..in returns a list of keys on the object being iterated, whereas for..of returns a list of values of the numeric properties of the object being iterated.
String, Array, array-like objects (e.g., arguments or NodeList), TypedArray, Map, Set, and user-defined iterables.Example:
// for..in
const list = [10, 20, 30];
for (let key in list) {
console.log(key); // "0", "1", "2",
}
// for..of
for (let value of list) {
console.log(value); // "10", "20", "30"
}
// Works on any iterable: arrays, strings, Map, Set
for (let char of "hello") {
console.log(char); // "h", "e", "l", "l", "o"
}
⚝ Try this example on CodeSandbox
1. forEach():
It takes a callback function and run that callback function on each element of array one by one. Basically forEach works as a traditional for loop looping over the array and providing array elements to do operations on them.
let numbers = [10, 20, 30];
numbers.forEach(function (number, index) {
console.log(number + " comes at " + index);
});
// Output
10 comes at 0
20 comes at 1
30 comes at 2
2. filter():
The main difference between forEach() and filter() is that forEach just loop over the array and executes the callback but filter executes the callback and check its return value. If the value is true element remains in the resulting array but if the return value is false the element will be removed for the resulting array.
Note: filter does not update the existing array it will return a new filtered array every time.
let numbers = [10, 20, 30];
let result = numbers.filter(function (number) {
return number !== 20;
});
console.log(result);
// Output
[10, 30]
3. map():
map() like filter() & forEach() takes a callback and run it against every element on the array but whats makes it unique is it generate a new array based on your existing array.
Like filter(), map() also returns an array. The provided callback to map modifies the array elements and save them into the new array upon completion that array get returned as the mapped array.
let numbers = [10, 20, 30];
let mapped = numbers.map(function (number) {
return number * 10;
});
console.log(mapped);
// Output
[100, 200, 300]
4. reduce():
reduce() method of the array object is used to reduce the array to one single value.
let numbers = [10, 20, 30];
let sum = numbers.reduce(function (sum, number) {
return sum + number;
});
console.log(sum); // Output: 60
Key Rules
map()filter()reduce()forEach()forEach() when you need a result — use map()/filter()/reduce() instead⚝ Try this example on CodeSandbox
JSON is an acronym for JavaScript Object Notation, and is "an open standard data interchange format".
JSON array represents ordered list of values. JSON array can store multiple values. It can store string, number, boolean or object in JSON array.
// Empty JSON array
const empty = [ ];
// JSON Array of Numbers
const numbers = [12, 34, 56, 43, 95];
// JSON Array of Objects
{
"employees": [
{ "name": "Kabir Dixit", "email": "kabir.dixit@gmail.com", "age": 23 },
{ "name": "Mukta Bhagat", "email": "mukta.bhagat@gmail.com", "age": 28 },
{ "name": "Sakshi Ramakrishnan", "email": "sakshi.ramakrishnan@gmail.com", "age": 33 }
]
}
// access array values
console.log(employees[0].name) // Kabir Dixit
JSON.parse() function will use string and converts to JSON object and if it parses invalidate JSON data, it throws an exception ( Uncaught SyntaxError: Unexpected string in JSON ).
function isValidJson(json) {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
}
console.log(isValidJson("{}")); // true
console.log(isValidJson("abc")); // false
⚝ Try this example on CodeSandbox
When sending data to a web server, the data has to be in a string format. The JSON.stringify() method converts a JavaScript object or value to a JSON string format.
const user = {'name': 'Shashi Meda', 'email': 'shashi.meda@email.com', 'age': 28}
console.log(JSON.stringify(user)); // {"name":"Shashi Meda","email":"shashi.meda@email.com","age":28}
When receiving the data from a web server, the data is always in a string format. But you can convert this string value to javascript object using JSON.parse() method.
const user = '{"name": "Shashi Meda", "email": "shashi.meda@email.com", "age": 28}'
console.log(JSON.parse(user));// {'name': 'Shashi Meda', 'email': 'shashi.meda@email.com', 'age': 28}
The purpose of the compare function is to define an alternative sort order. When the sort() function compares two values, it sends the values to the compare function, and sorts the values according to the returned (negative, zero, positive) value.
If omitted, the array elements are converted to strings, then sorted according to each character's Unicode code point value.
const numbers = [1, 2, 5, 3, 4];
numbers.sort((a, b) => b - a);
console.log(numbers); // [5, 4, 3, 2, 1]
⚝ Try this example on CodeSandbox
.forEach loop and a .map() loop and why you would pick one versus the other?To understand the differences between the two, Let us look at what each function does.
1. Array.forEach():
const numbers = [10, 20, 30];
const doubled = numbers.forEach((num, index) => {
return num * 2;
});
console.log(doubled) // undefined
2. Array.map():
const numbers = [10, 20, 30];
const doubled = numbers.map(num => {
return num * 2;
});
console.log(doubled) // [20, 40, 60]
The main difference between .forEach and .map() is that .map() returns a new array. If you need the result, but do not wish to mutate the original array, .map() is the clear choice. If you simply need to iterate over an array, forEach is a fine choice.
⚝ Try this example on CodeSandbox
The unshift() method adds one or more elements to the beginning of an array and returns the new length of the array.
Example:
const numbers = [10, 20, 30];
console.log(numbers.unshift(40, 50)); // 5
console.log(numbers); // [40, 50, 10, 20, 30]
⚝ Try this example on CodeSandbox
The rest parameter is used to represent an indefinite number of arguments as an array. The important point here is only the function's last parameter can be a "rest parameter".
This feature has been introduced to reduce the boilerplate code that was induced by the arguments.
Example:
function sum(...args) {
return args.reduce((previous, current) => {
return previous + current;
});
}
console.log(sum(10)); // 10
console.log(sum(10, 20)); // 30
console.log(sum(10, 20, 30)); // 60
⚝ Try this example on CodeSandbox
The rest parameter should be the last argument, as its job is to collect all the remaining arguments into an array.
Example: If you define a function like below it does not make any sense and will throw an SyntaxError.
function display(a, ...args, b) {
console.log(a);
for (let i = 0; i < args.length; i++) {
console.log(args[i]);
}
console.log(b);
}
display(10, 20, 30, 40, 50);
// Output
SyntaxError: Rest element must be last element
⚝ Try this example on CodeSandbox
[] and new Array() are two different ways of creating an array, but they are functionally equivalent.
The primary difference between them is in how they are created and in their behavior when used with certain methods.
[] is a shorthand for creating a new array. It is the preferred way to create an array in most cases, because it's more concise and easier to read. For example:
const myArray = []; // create a new empty array
On the other hand, new Array() is a constructor function that creates a new array object. It can be used to create an array of a specific length or with specific elements. For example:
const myArray = new Array(); // create a new empty array
const myOtherArray = new Array(3); // create a new array with a length of 3
const myThirdArray = new Array("a", "b", "c"); // create a new array with three elements
One potential pitfall of using new Array() is that it can be ambiguous when you pass a single argument to the constructor. For example, new Array(3) creates an array with a length of 3, but new Array("3") creates an array with a single element, the string "3". This is because the argument is treated as the value of the first element when it's a non-negative integer, but as the length of the array when it's a string or a negative integer.
In summary, [] is the preferred way to create a new array in JavaScript, while new Array() is an alternative way that can be used when you need more control over the array's length or contents.
Array.from(), Array.of() and Array.isArray() methods?1. Array.from():
Array.from() creates a new, shallow-copied array from an array-like or iterable object such as a Set, Map, NodeList, or string.
// From a string
console.log(Array.from('hello')); // ['h', 'e', 'l', 'l', 'o']
// From a Set
const set = new Set([1, 2, 3, 2, 1]);
console.log(Array.from(set)); // [1, 2, 3]
// With a map function
console.log(Array.from([1, 2, 3], x => x * 2)); // [2, 4, 6]
// From array-like object
console.log(Array.from({ length: 3 }, (_, i) => i + 1)); // [1, 2, 3]
2. Array.of():
Array.of() creates a new array from a variable number of arguments, regardless of number or type. It fixes the ambiguity of new Array() with a single numeric argument.
console.log(Array.of(7)); // [7] (one element)
console.log(new Array(7)); // [ , , , , , , ] (empty array of length 7!)
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
3. Array.isArray():
Array.isArray() returns true if the passed value is an array, false otherwise. It is more reliable than instanceof when working across different frames or windows.
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray('hello')); // false
console.log(Array.isArray({ length: 3 })); // false
console.log(Array.isArray(new Array())); // true
Control flow determines the order in which statements are executed. JavaScript provides three main categories:
| Category | Statements |
|---|---|
| Conditional | if, else if, else, switch, ternary (? :) |
| Looping / Iteration | for, while, do...while, for...in, for...of |
| Jump / Transfer | break, continue, return, throw |
By default, JavaScript executes code top-to-bottom (sequential flow). Control flow statements allow branching and repetition, enabling complex logic.
Example:
// Conditional branching
const score = 78;
if (score >= 90) {
console.log("Grade: A");
} else if (score >= 75) {
console.log("Grade: B"); // ← executed
} else {
console.log("Grade: C");
}
// Looping
const students = ["Alice", "Bob", "Carol"];
for (let i = 0; i < students.length; i++) {
console.log(students[i]);
}
// Jump: exit loop early
for (let i = 0; i < 10; i++) {
if (i === 5) break;
console.log(i); // 0, 1, 2, 3, 4
}
if...else if...else ladder work, and what are its performance considerations?The if...else if...else ladder evaluates conditions sequentially from top to bottom and executes the first block whose condition is true. Once a matching branch executes, all remaining else if and else blocks are skipped — making it short-circuit by design.
Performance consideration: JavaScript evaluates each condition until one is truthy. Placing the most frequently matched condition first reduces the number of comparisons at runtime.
Example:
function classify(temperature) {
if (temperature < 0) {
return "Freezing";
} else if (temperature < 10) {
return "Cold";
} else if (temperature < 20) {
return "Mild";
} else if (temperature < 30) {
return "Warm";
} else {
return "Hot";
}
}
console.log(classify(-5)); // Freezing
console.log(classify(15)); // Mild
console.log(classify(35)); // Hot
Falsy vs. Truthy awareness:
// These values are falsy in JavaScript:
// false, 0, "", null, undefined, NaN
const value = 0;
if (value) {
console.log("Truthy");
} else {
console.log("Falsy"); // ← executed because 0 is falsy
}
switch statement instead of if...else if, and what are its pitfalls?A switch statement compares a single expression against multiple discrete values using strict equality (===). It is preferable over if...else if when:
Pitfall — Fall-through: Without a break statement, execution "falls through" to the next case, which is a common source of bugs.
Example:
const day = "Monday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("Weekday");
break;
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Unknown day");
}
// Output: Weekday
Intentional fall-through example:
// Grouping cases that share behavior — intentional fall-through
const statusCode = 404;
switch (statusCode) {
case 400:
case 401:
case 403:
case 404:
console.log("Client error");
break;
case 500:
case 502:
case 503:
console.log("Server error");
break;
default:
console.log("Unknown status");
}
// Output: Client error
Pitfall — switch uses ===, not ==:
const x = "1";
switch (x) {
case 1:
console.log("Number 1"); // NOT matched
break;
case "1":
console.log("String '1'"); // ← matched
break;
}
for, for...in, and for...of loops with practical examples.| Loop | Iterates Over | Use Case |
|---|---|---|
for | Index-based counter | Arrays, precise iteration control |
for...in | Enumerable property keys | Object properties (avoid for arrays) |
for...of | Iterable values (arrays, strings, Maps, Sets, generators) | Arrays, strings, and other iterables |
Example:
// --- for loop: classic index-based ---
const arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
console.log(`Index ${i}: ${arr[i]}`);
}
// Index 0: 10 | Index 1: 20 | Index 2: 30
// --- for...in: object property keys ---
const user = { name: "Alice", age: 30, role: "admin" };
for (const key in user) {
console.log(`${key}: ${user[key]}`);
}
// name: Alice | age: 30 | role: admin
// --- for...of: iterable values ---
const fruits = ["apple", "banana", "cherry"];
for (const fruit of fruits) {
console.log(fruit);
}
// apple | banana | cherry
// for...of also works on strings
for (const char of "hello") {
process.stdout.write(char + " ");
}
// h e l l o
// for...of with Map
const scores = new Map([["Alice", 95], ["Bob", 87]]);
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
Why avoid for...in on arrays?
const arr = [1, 2, 3];
arr.custom = "oops"; // accidentally adding a property
for (const key in arr) {
console.log(key); // "0", "1", "2", "custom" ← includes inherited/custom keys!
}
for (const val of arr) {
console.log(val); // 1, 2, 3 ← only values, safe
}
Real-World Use Case:
In a Node.js data-processing pipeline, for...of with await (inside an async function) iterates over a paginated API result set sequentially — something forEach cannot do since it ignores returned Promises. for...in is used to dynamically enumerate config object keys when serializing environment-specific settings.
while and do...while loops differ, and when should each be used?| Loop | Condition Check | Minimum Executions | Use When |
|---|---|---|---|
while | Before each iteration | 0 (may never run) | Number of iterations is unknown; condition may be false initially |
do...while | After each iteration | 1 (always runs once) | Body must execute at least once regardless of condition |
Example:
// --- while loop ---
let stock = 5;
while (stock > 0) {
console.log(`Items remaining: ${stock}`);
stock--;
}
// Runs 5 times. If stock were 0 initially, it would never run.
// --- do...while loop ---
let attempts = 0;
let pin;
do {
pin = prompt("Enter 4-digit PIN:"); // Must show prompt at least once
attempts++;
} while (pin !== "1234" && attempts < 3);
console.log(attempts === 3 ? "Locked out" : "Access granted");
Infinite loop guard — always ensure the condition can become false:
// DANGER: this never terminates
// while (true) { }
// SAFE: controlled exit with break
let counter = 0;
while (true) {
counter++;
if (counter >= 5) break;
}
console.log(counter); // 5
break and continue statements, and how do they interact with nested loops?break: Immediately terminates the innermost loop or switch statement and transfers control to the statement following it.continue: Skips the current iteration of the innermost loop and jumps to the next iteration's condition check.Example:
// --- break: stop on first match ---
const inventory = [
{ id: 1, name: "Widget A", inStock: false },
{ id: 2, name: "Widget B", inStock: true },
{ id: 3, name: "Widget C", inStock: true },
];
let firstAvailable = null;
for (const item of inventory) {
if (item.inStock) {
firstAvailable = item;
break; // stop searching once found
}
}
console.log(firstAvailable); // { id: 2, name: "Widget B", inStock: true }
// --- continue: skip unwanted items ---
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = [];
for (const n of numbers) {
if (n % 2 !== 0) continue; // skip odd numbers
evens.push(n);
}
console.log(evens); // [2, 4, 6, 8, 10]
Labeled statements — breaking outer loops:
// Without labels, break only exits the inner loop
outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outerLoop; // exits BOTH loops
}
console.log(`i=${i}, j=${j}`);
}
}
// i=0,j=0 | i=0,j=1 | i=0,j=2 | i=1,j=0 ← stops here
Real-World Use Case:
In a search autocomplete engine, break short-circuits a loop over a large dataset once the desired number of suggestions is collected, preventing unnecessary iterations. continue is used in data-cleaning pipelines to skip null or malformed records without nesting the entire logic in an extra if block — keeping cyclomatic complexity low and the code easier to review.
if...else for conditional expressions?The ternary operator (condition ? expressionIfTrue : expressionIfFalse) is JavaScript's only three-operand operator. It evaluates a condition and returns one of two expressions. Unlike if...else (a statement), the ternary is an expression — it produces a value and can be used inline.
Example:
// if...else statement — cannot be used inline
let label;
const isLoggedIn = true;
if (isLoggedIn) {
label = "Logout";
} else {
label = "Login";
}
// Ternary expression — equivalent, concise, assignable
const label2 = isLoggedIn ? "Logout" : "Login";
console.log(label2); // "Logout"
// Inline in JSX (React)
// <button>{isLoggedIn ? "Logout" : "Login"}</button>
// Nested ternary — use sparingly, prefer if...else for readability
const score = 72;
const grade = score >= 90 ? "A"
: score >= 75 ? "B"
: score >= 60 ? "C"
: "F";
console.log(grade); // "C"
When NOT to use ternary:
// Bad: using ternary for side effects reduces readability
isLoggedIn ? doLogout() : doLogin();
// Better: use if...else for side effects
if (isLoggedIn) {
doLogout();
} else {
doLogin();
}
Real-World Use Case:
In React, the ternary operator is the standard pattern for conditional rendering within JSX since JSX is an expression context — if statements cannot appear inside {} interpolation. For example, rendering a loading spinner vs. a data table based on isLoading state is universally handled with a ternary.
??) and optional chaining (?.) operators, and how do they relate to control flow?Both operators introduced in ES2020 reduce verbose defensive if checks for null/undefined values:
??): Returns the right-hand operand when the left is null or undefined (not other falsy values like 0 or "").?.): Short-circuits and returns undefined instead of throwing a TypeError when accessing a property on null or undefined.Example:
// --- Nullish Coalescing ?? ---
const userAge = 0; // falsy but valid!
// Old approach (bug: treats 0 as falsy)
const age1 = userAge || 18; // 18 ← WRONG: overwrites valid 0
// Correct approach
const age2 = userAge ?? 18; // 0 ← CORRECT: 0 is not null/undefined
console.log(age1, age2); // 18, 0
// --- Optional Chaining ?. ---
const user = {
profile: {
address: null
}
};
// Without optional chaining — throws TypeError
// console.log(user.profile.address.city); // TypeError!
// With optional chaining — safe
console.log(user.profile?.address?.city); // undefined (no error)
// Works on methods too
const result = user.getPermissions?.(); // undefined if method doesn\'t exist
// Works on arrays
const first = user.tags?.[0]; // undefined if tags is null/undefined
// --- Combined usage ---
const city = user.profile?.address?.city ?? "Unknown City";
console.log(city); // "Unknown City"
Real-World Use Case:
When consuming third-party REST APIs or GraphQL responses, deeply nested fields may be absent depending on the query. Optional chaining eliminates dozens of guard clauses like if (response && response.data && response.data.user). Nullish coalescing is critical when a user-configurable setting has 0 or "" as a valid value — falling back with || would incorrectly override those legitimate settings.
A labeled statement is an identifier followed by a colon (:) placed before a loop or block. Labels can be referenced by break and continue to control the flow of outer loops from within a nested loop. They are rarely used — but in specific nested-iteration scenarios they can be clearer than managing manual flags.
Example:
// --- Using label with break to exit a matrix search ---
function findValue(matrix, target) {
let found = false;
search: for (let row = 0; row < matrix.length; row++) {
for (let col = 0; col < matrix[row].length; col++) {
if (matrix[row][col] === target) {
console.log(`Found at [${row}][${col}]`);
found = true;
break search; // exit both loops immediately
}
}
}
return found;
}
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
findValue(matrix, 5); // Found at [1][1]
// --- Using label with continue ---
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) continue outer; // skip rest of inner loop, go to next outer iteration
console.log(`i=${i}, j=${j}`);
}
}
// i=0,j=0 | i=1,j=0 | i=2,j=0
Alternative to a flag variable:
// Without labels — requires a flag
let found = false;
for (let i = 0; i < rows && !found; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === target) { found = true; break; }
}
}
// With labels — no flag needed, intent is clearer
searchGrid: for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (grid[i][j] === target) break searchGrid;
}
}
Real-World Use Case:
In a pathfinding algorithm (e.g., BFS/DFS on a 2D game map) or a seat-selection engine scanning rows and seats in a stadium booking system, a labeled break exits all nested loops the moment a valid seat is located — avoiding a boolean flag variable that would otherwise pollute the outer scope and add an extra condition to every loop header.
JavaScript's logical operators (&&, ||, ??) use short-circuit evaluation: the right-hand operand is only evaluated if necessary. This is not just a performance optimization — it is a widely used control flow mechanism.
| Operator | Short-circuits when left side is | Returns |
|---|---|---|
&& (AND) | falsy | Left operand (skips right) |
|| (OR) | truthy | Left operand (skips right) |
?? (Nullish) | not null/undefined | Left operand (skips right) |
Example:
// --- && short-circuit: right side only runs if left is truthy ---
const user = { isAdmin: true };
user.isAdmin && console.log("Admin panel loaded"); // ← runs
null && console.log("This never runs"); // ← skipped
// --- || short-circuit: right side only runs if left is falsy ---
const config = null;
const settings = config || { theme: "dark", lang: "en" }; // default fallback
console.log(settings); // { theme: "dark", lang: "en" }
// --- Practical: guard before calling a function ---
function logError(err) {
err && err.message && console.error(err.message);
}
logError(null); // nothing happens (safe)
logError({ message: "Failed" }); // "Failed"
// --- Chaining for conditional execution (React pattern) ---
const isLoading = false;
const data = [1, 2, 3];
// Equivalent to: if (!isLoading && data.length > 0) render()
const output = !isLoading && data.length > 0 && "Render data";
console.log(output); // "Render data"
Gotcha — using && for side effects:
// This is valid but can be confusing:
isAuthenticated && fetchDashboard();
// Prefer explicit if for clarity in complex flows:
if (isAuthenticated) {
fetchDashboard();
}
Real-World Use Case:
In React, && short-circuit is the idiomatic pattern for conditional rendering: {isLoggedIn && <UserMenu />} renders the component only when isLoggedIn is truthy, with no JSX if block needed. The || operator is universally used for default parameter fallbacks before optional chaining became standard. In Node.js middleware chains, && guards ensure a dependency (e.g., a database connection) is available before proceeding to query logic.
Arrows is a new syntax for functions, which brings several benefits:
this to the surrounding code's context=> is shorter and simpler than function, although stylistic issues are often subjectiveExample 01: Arrow Function with No Argument
If a function doesn't take any argument, then you should use empty parentheses.
let greet = () => console.log('Hello');
greet(); // Hello
Example 02: Arrow Function with One Argument
If a function has only one argument, you can omit the parentheses.
let greet = x => console.log(x);
greet('Hello'); // Hello
Example 03: Arrow Function as an Expression
You can also dynamically create a function and use it as an expression.
let age = 25;
let welcome = (age < 18) ?
() => console.log('Baby') :
() => console.log('Adult');
welcome(); // Adult
Example 04: Multiline Arrow Functions
If a function body has multiple statements, you need to put them inside curly brackets {}.
let area = (r) => {
const pi = 3.14;
return pi * r * r;
}
let result = area(10);
console.log(result); // 314
Note: Unlike regular functions, arrow functions do not have their own this. The value of this inside an arrow function remains the same throughout the lifecycle of the function and is always bound to the value of this in the closest non-arrow parent function.
⚝ Try this example on CodeSandbox
The main advantage of using an arrow function as a method inside a constructor is that the value of this gets set at the time of the function creation and can't change after that. So, when the constructor is used to create a new object, this will always refer to that object.
Example:
const Person = function(firstName) {
this.firstName = firstName;
this.sayName1 = function() { console.log(this.firstName); }; // regular function
this.sayName2 = () => { console.log(this.firstName); }; // arrow function
};
const john = new Person('John');
const dave = new Person('Dave');
john.sayName1(); // John
john.sayName2(); // John
// Regular function: 'this' can be changed with call/apply/bind
john.sayName1.call(dave); // Dave
john.sayName2.call(dave); // John — arrow function ignores call/apply/bind
// Detached from object: regular function loses 'this'
const fn1 = john.sayName1;
fn1(); // undefined
const fn2 = john.sayName2;
fn2(); // John — arrow function retains original 'this'
Function, Method and Constructor calls in JavaScript?1. Functions: The simplest usages of function call:
function display(name) {
return "Hello " + name;
}
display("World"); // "Hello World"
2. Methods: in JavaScript are nothing more than object properties that are functions.
var obj = {
display : function() {
return "Hello " + this.name;
},
name: 'Minali Peri'
}
obj.display(); // "Hello Minali Peri"
3. Constructors: Like function and method, constructors are defined with function.
function Employee(name, age) {
this.name = name;
this.age = age;
}
var emp1 = new Employee('Drishya Sama', 28);
emp1.name; // "Drishya Sama"
emp1.age; // 28
Unlike function calls and method calls, a constructor call new Employee('Drishya Sama', 28) creates a new object and passes it as the value of this, and implicitly returns the new object as its result. The primary role of the constructor function is to initialize the object.
An arrow function is a shorter syntax for a function expression and does not have its own this, arguments, super, or new.target. These function are best suited for non-method functions, and they cannot be used as constructors.
Arrow functions in ES6 has two limitations:
When should not use Arrow Functions:
1. Object methods:
The counter object has two methods: current() and next(). The current() method returns the current counter value and the next() method returns the next counter value.
// Usin arrow function
const counter = {
count: 0,
next: () => ++this.count,
current: () => this.count
};
console.log(counter.next()); // NaN
2. Event handlers:
If we click the button, we would get a TypeError. It is because this is not bound to the button, but instead bound to its parent scope.
let button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
3. Prototype methods:
The this value in these next() and current() methods reference the global object. Since the this value inside the methods to reference the Counter object, it needs to use the regular functions instead
function Counter() {
this.count = 0;
}
Counter.prototype.next = () => {
return this.count;
};
Counter.prototype.current = () => {
return ++this.next;
}
4. Functions that use the arguments object:
Arrow functions don't have the arguments object. Therefore, if a function that uses arguments object, you cannot use the arrow function.
const concat = (separator) => {
let args = Array.prototype.slice.call(arguments, 1);
return args.join(separator);
}
⚝ Try this example on CodeSandbox
JavaScript function objects are used to define a piece of JavaScript code. This code can be called within a JavaScript code as and when required.
Javascript Function Objects Property:
| Name | Description |
|---|---|
| arguments | An array corresponding to the arguments passed to a function. |
| arguments.callee | Refers the currently executing function. |
| arguments.length | Refers the number of arguments defined for a function. |
| constructor | Specifies the function that creates an object. |
| length | The number of arguments defined by the function. |
| prototype | Allows adding properties to a Function object. |
In javaScript, functions can be stored as a variable inside an object or an array as well as it can be passed as an argument or be returned by another function. That makes function first-class function in JavaScript.
A first-class function means functions are treated as values — they can be:
const greet = function() { return "Hello"; }arr.map(fn)function outer() { return function inner() {} }const obj = { fn: () => {} }JavaScript fully supports first-class functions, which enables patterns like callbacks, higher-order functions, and closures.
Example 01: Assign a function to a variable
const message = function() {
console.log("Hello World!");
}
message(); // Invoke it using the variable
Example 02: Pass a function as an Argument
function sayHello() {
return "Hello, ";
}
function greeting(helloMessage, name) {
console.log(helloMessage() + name);
}
// Pass `sayHello` as an argument to `greeting` function
greeting(sayHello, "JavaScript!");
Example 03: Return a function
function sayHello() {
return function() {
console.log("Hello!");
}
}
Example 04: Using a variable
const sayHello = function() {
return function() {
console.log("Hello!");
}
}
const myFunc = sayHello();
myFunc();
Example 05: Using double parentheses
function sayHello() {
return function() {
console.log("Hello!");
}
}
sayHello()();
We are using double parentheses ()() to invoke the returned function as well.
⚝ Try this example on CodeSandbox
A Higher-Order function is a function that receives a function as an argument or returns the function as output.
For example, Array.prototype.map(), Array.prototype.filter() , Array.prototype.forEach() and Array.prototype.reduce() are some of the Higher-Order functions in javascript.
Example 01: .map()
const array = [10, 20, 30];
const result = array.map(function (item) {
return item * 2;
});
console.log(result); // [20, 40, 60]
Example 02: .filter()
const randomNumbers = [4, 11, 42, 14, 39];
const filteredArray = randomNumbers.filter((number) => {
return number > 15;
});
console.log(filteredArray); // [42, 39]
Example 03: .forEach()
const numbers = [28, 77, 45];
numbers.forEach((number) => {
console.log(number);
});
Example 04: .reduce()
const arrayOfNumbers = [10, 20, 30];
const sum = arrayOfNumbers.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
});
console.log("Sum: " + sum); // 60
⚝ Try this example on CodeSandbox
Unary function (i.e. monadic) is a function that accepts exactly one argument. It stands for single argument accepted by a function.
// Unary function
const unaryFunction = (number) => number + 10;
console.log(unaryFunction(10)); // 20
⚝ Try this example on CodeSandbox
Currying is the process of taking a function with multiple arguments and turning it into a sequence of functions each with only a single argument.
In other words, when a function, instead of taking all arguments at one time, takes the first one and return a new function that takes the second one and returns a new function which takes the third one, and so forth, until all arguments have been fulfilled.
// Normal function
const add = (a, b, c) => {
return a + b + c;
};
console.log(add(10, 10, 10)); // 30
// Currying function
const addCurry = (a) => {
return (b) => {
return (c) => {
return a + b + c;
};
};
};
console.log(addCurry(20)(20)(20)); // 60
Note: Curried functions are great to improve code re-usability and functional composition.
⚝ Try this example on CodeSandbox
Pure functions are functions that accept an input and returns a value without modifying any data outside its scope(Side Effects). Its output or return value must depend on the input/arguments and pure functions must return a value.
Example: Pure Function
It is a pure function because you always get a Hello <name> as output for the <name> pass as an input.
// Pure Function
function sayGreeting(name) {
return `Hello ${name}`;
}
console.log(sayGreeting("World"));
Example: Not Pure Function
The function's output now depends on an outer state called greeting. What if someone changes the value of the greeting variable to Hola? It will change the output of the sayGreeting() function even when you pass the same input.
let greeting = "Hello";
function sayGreeting(name) {
return `${greeting} ${name}`;
}
A function must pass two tests to be considered pure:
Benefits:
Memoization is a programming technique which attempts to increase a function's performance by caching its previously computed results.
Each time a memoized function is called, its parameters are used to index the cache. If the data is present, then it can be returned, without executing the entire function. Otherwise the function is executed and then the result is added to the cache.
// Memoized function to Add Number
const memoizedAdd = () => {
let cache = {};
return (number) => {
if (number in cache) {
console.log('Fetching from cache: ');
return cache[number];
}
else {
console.log('Calculating result: ');
let result = number + 10;
cache[number] = result;
return result;
}
}
}
// returned function from memoizedAdd
const sum = memoizedAdd();
console.log(sum(10)); // Calculating result: 20
console.log(sum(10)); // Fetching from cache: 20
⚝ Try this example on CodeSandbox
The arguments object is an Array-like object ( arguments ) accessible inside functions that contains the values of the arguments passed to that function.
Example:
/**
* Arguments Object
*/
function sum() {
let total = 0;
for (let i = 0, len = arguments.length; i < len; ++i) {
total += arguments[i];
}
return total;
}
sum(10, 20, 30); // returns 60
⚝ Try this example on CodeSandbox
The length property indicates the number of parameters expected by the function.
// function.length
function fun1() {}
console.log(fun1.length); // 0
function fun2(arg1, arg2) {}
console.log(fun2.length); // 2
⚝ Try this example on CodeSandbox
The bind() method creates a new function, when invoked, has the this sets to a provided value. The bind() method allows an object to borrow a method from another object without making a copy of that method. This is known as function borrowing in JavaScript.
Example:
/**
* bind() function
*/
const person = {
firstName: "Chhavi",
lastName: "Goswami",
fullName: function () {
return this.firstName + " " + this.lastName;
}
};
const member = {
firstName: "Vasuda",
lastName: "Sahota"
};
let fullName = person.fullName.bind(member);
console.log(fullName()); // Vasuda Sahota
⚝ Try this example on CodeSandbox
An anonymous function is a function without a name. Anonymous functions are commonly assigned to a variable name or used as a callback function.
Example 01: Anonymous function
let show = function () {
console.log("Anonymous function");
};
show();
Example 02: anonymous functions as arguments
setTimeout(function () {
console.log("Execute later after 1 second");
}, 1000);
Example 03: Immediately invoked function execution
const person = {
firstName: "Ayaan",
lastName: "Memon"
};
(function () {
console.log(person.firstName + " " + person.lastName); // Ayaan Memon
})(person);
Example 04: Arrow functions
let add = (a, b) => a + b;
add(10, 20); // 30
⚝ Try this example on CodeSandbox
function foo() {} and var foo = function() {}?1. Function Declaration:
Function declarations are evaluated upon entry into the enclosing scope, before any step-by-step code is executed. The function's name (foo) is added to the enclosing scope.
foo(); // Function Declaration Example!
function foo() {
console.log("Function Declaration Example!");
}
2. Function Expression:
Function expressions are evaluated as part of the step-by-step code, at the point where they appear. That one creates a function with no name, which it assigns to the foo variable.
foo(); // TypeError: foo is not a function
var foo = function() {
console.log(typeof foo); // function
};
console.log(typeof foo); // undefined
⚝ Try this example on CodeSandbox
Function Declarations:
A declared function is "saved for later use", and will be executed later, when it is invoked (called).
// Function declaration
function add(num1, num2) {
return num1 + num2;
}
function is only declared here. For using it, it must be invoked using function name. e.g add(10, 20);
Function Expression:
A function expression can be stored in a variable:
// Function expression
var add = function (num1, num2) {
return num1 + num2;
};
After a function expression has been stored in a variable, the variable can be used as a function. Functions stored in variables do not need function names. They are always invoked (called) using the variable name.
Difference:
Function declarations load before any code is executed while Function expressions load only when the interpreter reaches that line of code.var statement, function declarations are hoisted to the top of other code. Function expressions aren't hoisted, which allows them to retain a copy of the local variables from the scope where they were defined.Benefits of Function Expressions:
There are several different ways that function expressions become more useful than function declarations.
1. Function:
A function is a piece of code that is called by name and function itself not associated with any object and not defined inside any object. It can be passed data to operate on (i.e. parameter) and can optionally return value.
Example:
// Function
function message(msg) {
return msg;
}
// Call the function
message("Welcome to JavaScript");
Here, message() function call is not associated with object hence not invoked through any object.
2. Method:
A JavaScript method is a property of an object that contains a function definition. Methods are functions stored as object properties.
Example:
// Method
let employee = {
firstName: "Ajay",
lastName: "Nagi",
getName: function () {
return "Employe Name: " + this.firstName + " " + this.lastName;
}
};
// Call the method
console.log(employee.getName());
Here employee is an object and getName is a method which is associated with employee.
⚝ Try this example on CodeSandbox
Function binding ( .bind() ) is a method on the prototype of all functions in JavaScript. It allows to create a new function from an existing function, change the new function's this context, and provide any arguments you want the new function to be called with. The arguments provided to bind will precede any arguments that are passed to the new function when it is called.
Example:
// Function Binding
const person = {
firstName: "Nirupama",
lastName: "Randhawa",
getName: function () {
return this.firstName + " " + this.lastName;
}
};
const member = {
firstName: "Alisha",
lastName: "Chhabra"
};
let getName = person.getName.bind(member);
console.log(getName()); // Alisha Chhabra
⚝ Try this example on CodeSandbox
this works in JavaScript?The this keyword refers to an object. Which object depends on how this is being invoked (used or called). The this keyword refers to different objects depending on how it is used.
this refers to the object.this refers to the global object.this refers to the global object.this is undefined.this refers to the element that received the event.call(), apply(), and bind() can refer this to any object.Example:
// this keyword in object method
const person = {
firstName: "Nirupama",
lastName: "Randhawa",
getName: function () {
return this.firstName + " " + this.lastName;
}
};
Generator-Function:
A generator-function is defined like a normal function, but whenever it needs to generate a value, it does so with the yield keyword rather than return. The yield statement suspends function's execution and sends a value back to caller, but retains enough state to enable function to resume where it is left off. When resumed, the function continues execution immediately after the last yield run.
Syntax:
function* gen() {
yield 1;
yield 2;
yield 3;
...
}
Generator-Object:
Generator functions return a generator object. Generator objects are used either by calling the next method on the generator object or using the generator object in a "for in" loop.
Example:
// Generate Function
function* fun() {
yield 10;
yield 20;
yield 30;
}
// Calling the Generate Function
var gen = fun(); // returns a Generator object (doesn\'t run yet)
gen.next(); // { value: 10, done: false }
gen.next(); // { value: 20, done: false }
gen.next(); // { value: 30, done: false }
gen.next(); // { value: undefined, done: true }
⚝ Try this example on CodeSandbox
1. Generators/Yield:
Generators are objects created by generator functions — functions with an * (asterisk) next to their name. The yield keyword pauses generator function execution and the value of the expression following the yield keyword is returned to the generator's caller. It can be thought of as a generator-based version of the return keyword.
Example:
// Generator function
const generator = (function* () {
// waiting for .next()
const number = yield 5;
// waiting for .next()
console.log(number); // => 15
})();
console.log(generator.next()); // => { done: false, value: 5 }
console.log(generator.next(15)); // => { done: true, value: undefined }
2. Async/Await:
Async keyword is used to define an asynchronous function, which returns a AsyncFunction object.
Await keyword is used to pause async function execution until a Promise is fulfilled, that is resolved or rejected, and to resume execution of the async function after fulfillments. When resumed, the value of the await expression is that of the fulfilled Promise.
Key points:
Example:
// Async/Await
async function asyncFunction() {
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("resolved!"), 1000);
});
const result = await promise;
// wait till the promise resolves (*)
console.log(result); // "resolved!"
}
asyncFunction();
Generator and Async-await — Comparison:
⚝ Try this example on CodeSandbox
Two dates can be compared by converting them into numeric values using date.getTime() method to correspond to their time. Also,
the relational operators <, <=, >, >= can be used to compare JavaScript dates.
However, the equality operators ==, !=, ===, !== cannot be used to compare (the value of) dates because:
Example:
const date1 = new Date('2026-05-16T12:00:00');
const date2 = new Date('2026-05-16T12:00:00');
// Checking for equality
if (date1.getTime() === date2.getTime()) {
console.log("Both dates are exactly equal");
}
// Checking before or after
if (date1.getTime() < date2.getTime()) {
console.log("date1 is before date2");
}
⚝ Try this example on CodeSandbox
A closure is the combination of a function and the lexical environment within which that function was declared. i.e, it is an inner function that has access to the outer or enclosing function's variables.
Closure is useful in hiding implementation detail in JavaScript. In other words, it can be useful to create private variables or functions.
Lexical Scope:
In lexical scoping, free variables must belong to a parent scope. JavaScript uses lexical scoping — a function's scope is determined by where it is declared, not where it is called.
Example 01:
/**
* Lexical Scope
**/
function init() {
let name = "JavaScript closures"; // name is a local variable created by init
function displayName() {
// displayName() is the inner function, a closure
console.log(name); // use variable declared in the parent function
}
return displayName;
}
var closure = init();
closure(); // Output: "JavaScript closures"
As per the above code, the inner function displayName() has access to the variable name from the outer function init(), even after init() has returned. This is a closure.
⚝ Try this example on CodeSandbox
Example 02: Private Counter
A common use of closures is to create private state that cannot be accessed or modified directly from outside.
/**
* Closure - Private Counter
**/
function makeCounter() {
let count = 0; // private variable
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // Output: 2
// count is not directly accessible
console.log(typeof count); // Output: "undefined"
The variable count is encapsulated inside makeCounter and can only be modified through the returned methods.
A callback function is a function passed into another function as an argument, which is then invoked inside the outer function to complete some kind of routine or action.
Example:
/**
* Callback function
*/
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
function sayBye() {
console.log('Goodbye!');
}
greet('Alice', sayBye);
// Hello, Alice
// Goodbye!
Key points:
Promises or async/awaitNote: callbacks are often used to continue code execution after an asynchronous operation has completed — these are called asynchronous callbacks..
⚝ Try this example on CodeSandbox
Callback hell occurs when asynchronous operations are deeply nested inside one another, creating an unreadable "Pyramid of Doom"
Example:
// Callback hell
getUser(id, function(user) {
getOrders(user, function(orders) {
getDetails(orders[0], function(details) {
process(details, function(result) {
console.log(result); // deeply nested
});
});
});
});
Techniques for avoiding callback hell:
1. Promises — chain .then() instead of nesting:
getUser(id)
.then(user => getOrders(user))
.then(orders => getDetails(orders[0]))
.then(details => process(details))
.then(result => console.log(result))
.catch(err => console.error(err));
2. async/await — most readable, looks synchronous:
async function run() {
try {
const user = await getUser(id);
const orders = await getOrders(user);
const details = await getDetails(orders[0]);
const result = await process(details);
console.log(result);
} catch (err) {
console.error(err);
}
}
3. Named functions — extract callbacks into separate named functions:
function onDetails(details) { process(details).then(console.log); }
function onOrders(orders) { getDetails(orders[0]).then(onDetails); }
function onUser(user) { getOrders(user).then(onOrders); }
getUser(id).then(onUser);
4. Promise.all — run independent async operations in parallel:
const [users, orders] = await Promise.all([getUsers(), getOrders()]);
Note: Recommended approach: Use async/await for sequential logic and Promise.all for parallel operations.
JavaScript provides two built-in functions for URL encoding:
1. encodeURIComponent() — encodes a single URI component (query param value, path segment):
const param = 'hello world & name=John';
encodeURIComponent(param); // 'hello%20world%20%26%20name%3DJohn'
2. encodeURI() — encodes a full URL, preserving special URL characters (:, /, ?, #, &, =):
const url = 'https://example.com/search?q=hello world&lang=en';
encodeURI(url); // 'https://example.com/search?q=hello%20world&lang=en'
Key difference:
| Function | Encodes | Preserves |
|---|---|---|
encodeURIComponent | Everything except A-Z a-z 0-9 - _ . ! ~ * ' ( ) | Nothing special |
encodeURI | Spaces and unsafe chars | : / ? # & = @ ! $ ' ( ) * + , ; ~ |
When to use which:
const base = 'https://example.com/search';
const query = 'hello world & more';
// Correct: encode only the param value
const url = `${base}?q=${encodeURIComponent(query)}`;
// https://example.com/search?q=hello%20world%20%26%20more
// Wrong: encodeURI won't encode & in query values
encodeURI(`${base}?q=${query}`);
// https://example.com/search?q=hello%20world%20&%20more ← & breaks the query
Rule of thumb: Use encodeURIComponent for query parameter values; use encodeURI for a complete URL.
The decodeURI() function is used to decode a Uniform Resource Identifier (URI) previously created by encodeURI().
1. decodeURIComponent() — decodes a single encoded URI component:
decodeURIComponent('hello%20world%20%26%20name%3DJohn');
// 'hello world & name=John'
2. decodeURI() — decodes a fully encoded URL, leaving valid URL structure characters intact:
decodeURI('https://example.com/search?q=hello%20world&lang=en');
// 'https://example.com/search?q=hello world&lang=en'
Example — reading query params:
const url = 'https://example.com/search?q=hello%20world%20%26%20more';
const raw = url.split('?q=')[1];
decodeURIComponent(raw); // 'hello world & more'
Modern alternative — use URLSearchParams which handles encoding/decoding automatically:
const params = new URLSearchParams('q=hello%20world&lang=en');
params.get('q'); // 'hello world'
JavaScript does not support traditional function overloading (unlike Java or C#) where multiple functions can have the exact same name but different parameters. If you define two functions with the same name, the second definition will completely overwrite the first one.
Example:
function greet(name) { return `Hello, ${name}`; }
function greet(name, title) { return `Hello, ${title} ${name}`; }
greet('Alex'); // 'Hello, undefined Alex' — second definition always wins
Ways to simulate overloading:
1. Overloading by Argument Count (arguments.length):
function calculateArea(...args) {
if (args.length === 1) {
return Math.PI * args[0] * args[0]; // Circle area: 1 argument (radius)
}
if (args.length === 2) {
return args[0] * args[1]; // Rectangle area: 2 arguments (width, height)
}
}
console.log(calculateArea(5)); // 78.54 (Circle)
console.log(calculateArea(4, 5)); // 20 (Rectangle)
2. Overloading by Data Type (typeof / instanceof)
function add(a, b) {
if (typeof a === 'string') return a + b; // string concat
if (typeof a === 'number') return a + b; // numeric add
}
add(1, 2); // 3
add('foo', 'bar'); // 'foobar'
3. Overloading using an Options Object
function createUser({ name, age = 18, role = 'user' } = {}) {
return { name, age, role };
}
createUser({ name: 'Alice', age: 20 });
createUser({ name: 'Bob', age: 30, role: 'Admin' });
4. Rest parameters for variable argument count:
function sum(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
sum(1, 2); // 3
sum(1, 2, 3, 4); // 10
Recommended approach: Use the options object pattern for complex cases — it's readable, flexible, and self-documenting.
An IIFE (Immediately Invoked Function Expression) is a JavaScript function that is defined and executed immediately after its creation. It is a design pattern used to execute code immediately and isolate variables from the global scope.
Syntax:
(function () {
console.log('Runs immediately');
})();
// Arrow function version
(() => {
console.log('Also runs immediately');
})();
Example 01:
const result = (function () {
const message = 'Hello from IIFE';
return message;
})();
console.log(result); // 'Hello from IIFE'
console.log(typeof message); // 'undefined' — private to the IIFE
Example 02: Create a private scope
// Counter using IIFE
const counter = (function () {
let count = 0; // private
return {
increment() { return count++; },
value() { return count; }
};
})();
console.log(counter.increment()); // 0
console.log(counter.value()); // 1
//console.log(count); // ReferenceError: count is not defined
Use cases:
Data Privacy: Variables declared inside an IIFE cannot be accessed from the outside world. This protects your code from polluting the global scope.
Avoiding Naming Collisions: It prevents variables from accidentally overwriting other variables in third-party libraries or scripts.
State Preservation: Historically, it was used inside loops to lock in the current value of a variable during asynchronous operations.
Note:
With ES6 let/const and modules, IIFEs are less common today — block scoping and module scope solve the same problems. However, IIFEs are still useful for one-time initialization logic and encapsulating setup code.
Recursion is a technique in which a function calls itself until it reaches a base case (a condition that stops further calls). Every recursive function must have a base case to avoid infinite loops and stack overflow errors.
Example 01: Factorial
function factorial(n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive call
}
console.log(factorial(5)); // 120 (5 × 4 × 3 × 2 × 1)
console.log(factorial(0)); // 1
Example 02: Fibonacci
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(6)); // 8 (0, 1, 1, 2, 3, 5, 8)
Example 03: Flatten a nested array
function flattenArray(arr) {
return arr.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flattenArray(val)) : acc.concat(val),
[]);
}
console.log(flattenArray([1, [2, [3, [4]], 5]])); // [1, 2, 3, 4, 5]
Recursion vs Iteration:
Note: For deeply nested structures, iterative solutions with an explicit stack are preferred over recursion to avoid stack-overflow errors.
A nested function is a function defined inside another function. The inner function has access to the outer function's variables and parameters through closure, but the outer function cannot access the inner function's variables.
Example:
function outerFunction(outerValue) {
const outerVar = 'I am outer';
function innerFunction(innerValue) {
// inner can access outer\'s scope
console.log(outerVar); // 'I am outer'
console.log(outerValue); // passed argument
console.log(innerValue); // inner argument
}
innerFunction('I am inner');
}
outerFunction('hello');
Use case — private helper functions:
function processOrder(order) {
function validate(o) {
return o.quantity > 0 && o.price > 0;
}
function calculateTotal(o) {
return o.quantity * o.price;
}
if (!validate(order)) throw new Error('Invalid order');
return calculateTotal(order);
}
console.log(processOrder({ quantity: 3, price: 15 })); // 45
The inner helpers validate and calculateTotal are private to processOrder and not accessible from outside.
A side effect is any observable change that a function makes to the state outside its own scope — such as modifying a global variable, mutating an argument, writing to the DOM, or making a network request.
Functions with side effects:
let count = 0;
function increment() {
count++; // modifies external state — side effect
}
increment();
console.log(count); // 1
Pure function (no side effects):
function add(a, b) {
return a + b; // no external mutation, same input always gives same output
}
Common sources of side effects:
| Source | Example |
|---|---|
| Mutating arguments | arr.push(item) inside a function |
| Global state change | Modifying window or module-level variables |
| I/O operations | console.log, network calls, file writes |
| DOM manipulation | document.getElementById('x').textContent = '...' |
Why it matters: Functions with side effects are harder to test, reason about, and reuse. Functional programming favors isolating side effects to the edges of a system while keeping the core logic pure.
Point-free style (also called tacit programming) is a way of defining functions without explicitly mentioning the arguments they operate on. Instead, functions are composed of other functions using combinators like map, filter, reduce, or a compose/pipe utility.
Without point-free:
const numbers = [1, 2, 3, 4, 5];
// explicitly names the argument n
const doubled = numbers.map(n => n * 2);
With point-free:
const double = x => x * 2;
// double is passed directly — no explicit argument mentioned
const doubled = numbers.map(double);
More complex example:
const trim = str => str.trim();
const toUpperCase = str => str.toUpperCase();
const words = str => str.split(' ');
// Point-free pipeline using a pipe utility
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const process = pipe(trim, toUpperCase, words);
console.log(process(' hello world ')); // ['HELLO', 'WORLD']
Point-free style leads to cleaner, more declarative code but can reduce readability when overused.
Function composition is the process of combining two or more functions where the output of one function becomes the input of the next. It is a core concept in functional programming.
compose applies functions right-to-left; pipe applies them left-to-right.
compose:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const add10 = x => x + 10;
const double = x => x * 2;
const square = x => x * x;
// square( double( add10(5) ) ) => square(double(15)) => square(30) => 900
const transform = compose(square, double, add10);
console.log(transform(5)); // 900
pipe (left-to-right):
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// add10(5) => double(15) => square(30) => 900
const transform = pipe(add10, double, square);
console.log(transform(5)); // 900
Real-world example:
const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s+/g, '-');
const slugify = pipe(trim, toLower, removeSpaces);
console.log(slugify(' Hello World ')); // 'hello-world'
Lexical scope (also called static scope) means that the scope of a variable is determined at author time — by where the function is written in the source code, not where it is called from. JavaScript uses lexical scoping exclusively. When the JavaScript engine (V8) parses the source, it builds a scope chain based on the physical nesting of functions and blocks; this chain never changes at runtime.
Dynamic scope, by contrast, resolves variable references based on the call stack at runtime. Languages like Bash and early Lisp dialects use dynamic scope. JavaScript has no dynamic scope (though this is sometimes confused for it because it is determined by the call site — this binding is separate from scope).
Key rules of lexical scope:
ReferenceError is thrown (in strict mode) or an implicit global is created (sloppy mode).// Lexical scope example
const greeting = 'Hello'; // global scope
function outer() {
const name = 'Alice'; // outer function scope
function inner() {
const punctuation = '!'; // inner function scope
// inner can see: punctuation, name, greeting (chain lookup)
console.log(`${greeting}, ${name}${punctuation}`);
}
inner(); // 'Hello, Alice!'
// console.log(punctuation); // ReferenceError — outer cannot see inner\'s variables
}
outer();
Real-World Use Case:
React's hook system relies on lexical scope. When you write a custom hook like useCounter, the state variables declared inside it are lexically scoped to that hook invocation — they cannot bleed into unrelated components, making stateful logic safely composable and predictable in a large codebase.
var, let, and const with respect to scope.| Feature | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisted | Yes (as undefined) | Yes (TDZ — not initialised) | Yes (TDZ — not initialised) |
| Re-declarable | Yes | No | No |
| Re-assignable | Yes | Yes | No (binding; object contents can mutate) |
| Global property | Creates window.x | No | No |
Block scope means the variable only exists within the nearest enclosing {} — an if, for, while, or plain block. var ignores block boundaries and is scoped to the enclosing function (or the global object if outside any function).
function demonstrateScope() {
if (true) {
var funcScoped = 'var'; // hoisted to function top
let blockScoped = 'let'; // lives only inside this if-block
const immutable = 'const'; // same block scope as let
}
console.log(funcScoped); // 'var'
// console.log(blockScoped); // ReferenceError
// console.log(immutable); // ReferenceError
}
// Classic var-in-loop bug
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 0); // prints 3, 3, 3
}
// Fixed with let (each iteration gets its own binding)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 0); // prints 0, 1, 2
}
Real-World Use Case:
In large React/Node.js codebases, var inside for loops iterating over async operations (e.g., firing multiple API calls) was a notorious source of race-condition bugs because all callbacks shared the same i. Migrating to let (or closure-capturing patterns) eliminated entire categories of production bugs. ESLint's no-var rule enforces this at scale.
In ES6, let bindings are not subject to "variable hoisting", which means that let declarations do not move to the top of the current execution context.
Referencing the variable in the block before the initialization results in a ReferenceError (contrary to a variable declared with var, which will just have the undefined value). The variable is in a "temporal dead zone" from the start of the block until the initialization is processed.
console.log(x); // undefined — var is hoisted and initialised to undefined
var x = 5;
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
// TDZ also applies inside blocks
{
// TDZ for 'z' starts here ↓
// console.log(z); // ReferenceError
let z = 42; // TDZ ends here, z is now initialised
console.log(z); // 42
}
Real-World Use Case:
TDZ prevents a subtle bug in module initialisation cycles. If module A imports from module B and module B imports from module A, const exports are protected by TDZ, causing a clear ReferenceError at the problematic access point rather than silently producing undefined computations that corrupt data — making circular-dependency bugs far easier to diagnose in large monorepo setups.
A closure is a function that retains a reference to its lexical environment (the scope in which it was defined) even after that outer scope has finished executing. In V8, when a function is created, the engine attaches a hidden [[Environment]] slot pointing to the current scope's environment record. As long as the inner function is reachable, the garbage collector keeps the outer environment record alive.
Practically: closures "close over" variables — not values. They hold a live reference to the variable binding, so mutations to that variable are visible inside the closure.
function makeCounter(initialValue = 0) {
let count = initialValue; // this variable is closed over
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = initialValue; return count; },
value() { return count; },
};
}
const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.value()); // 11
// count is NOT accessible from outside
// console.log(count); // ReferenceError
Real-World Use Case:
The Redux store implementation (createStore) uses closures to encapsulate currentState, currentReducer, and listeners. External code can only interact with state through the returned dispatch, getState, and subscribe functions — a closure-based module pattern that enforces immutable, predictable state management at scale.
The Module Pattern (and its IIFE variant) uses closures to create private state — something JavaScript classes did not natively support before ES2022 private fields (#). An IIFE runs once, defines private variables, and returns a public API. Only the returned methods can access the private variables via closure.
Example:
// Classic Module Pattern (IIFE-based)
const BankAccount = (() => {
// Private state — not accessible from outside
let _balance = 0;
const _history = [];
function _log(action, amount) {
_history.push({ action, amount, balance: _balance, ts: Date.now() });
}
// Public API
return {
deposit(amount) {
if (amount <= 0) throw new RangeError('Amount must be positive');
_balance += amount;
_log('deposit', amount);
return this;
},
withdraw(amount) {
if (amount > _balance) throw new Error('Insufficient funds');
_balance -= amount;
_log('withdrawal', amount);
return this;
},
getBalance() { return _balance; },
getHistory() { return [..._history]; }, // defensive copy
};
})();
BankAccount.deposit(500).deposit(200).withdraw(100);
console.log(BankAccount.getBalance()); // 600
console.log(BankAccount._balance); // undefined — truly private
Real-World Use Case:
Before ES modules became ubiquitous, large libraries like jQuery, Lodash, and early Angular 1.x used the Module Pattern (IIFE + closure) to avoid polluting the global namespace and to encapsulate internal state. Today, ES modules provide file-level encapsulation, but the closure-based pattern is still used inside modules for class-alternative encapsulation and for factories returned from hooks or composables.
A closure memory leak occurs when a closure unintentionally keeps a large object (such as a DOM node, a large dataset, or a complete HTTP response) in memory by holding a reference in its closed-over scope long after the object is needed. The V8 garbage collector cannot reclaim the memory because the closure is still reachable.
Common patterns that cause leaks:
setInterval callbacks closing over large objects run indefinitely.// Leak: interval callback closes over largeData forever
function startPolling() {
const largeData = new Array(100_000).fill('payload');
const id = setInterval(() => {
console.log(largeData.length); // largeData is never freed
}, 1000);
// Missing: return id so the caller can clearInterval(id)
}
// Fix: return cleanup, or null out the reference
function startPollingFixed() {
const largeData = new Array(100_000).fill('payload');
const id = setInterval(() => {
process(largeData);
}, 1000);
return () => clearInterval(id); // caller must invoke this to free memory
}
const stopPolling = startPollingFixed();
// Later, when no longer needed:
stopPolling(); // interval cleared, largeData eligible for GC
React-specific pattern:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort(); // cleanup prevents stale closure + memory leak
}, []);
Real-World Use Case:
In Single Page Applications (SPAs), route navigation causes component unmounting. Event listeners or WebSocket message handlers registered on mount but not removed on unmount keep the entire component closure alive for the application's lifetime, causing memory growth that crashes long-running sessions (e.g., trading dashboards, kiosk apps). React's useEffect cleanup function pattern was designed specifically to address this.
An Immediately Invoked Function Expression (IIFE) is a function that is defined and called in a single expression. Its primary purpose is to create an isolated scope, preventing variable declarations from leaking into the surrounding (often global) scope.
(function() { /* body */ })();
// or with arrow functions:
(() => { /* body */ })();
When to still use IIFEs over ES modules:
type="module" and need scope isolation.window.MyLib while avoiding global pollution.Example:
// UMD wrapper (simplified) — the output of many bundlers
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? module.exports = factory() // CommonJS (Node.js)
: typeof define === 'function' && define.amd
? define(factory) // AMD (RequireJS)
: (global.MyLib = factory()); // Browser global
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
const VERSION = '1.0.0'; // private
return { VERSION, greet: (name) => `Hello, ${name}` }; // public API
}));
Real-World Use Case:
Webpack wraps each module in an IIFE in its legacy output mode to simulate module scope in environments lacking native ES module support (Internet Explorer, older Node.js). When targeting modern environments, Webpack and Rollup emit native ES modules instead, removing the IIFE overhead. Understanding this is essential when configuring output.libraryTarget in webpack configs for distributing npm packages.
When a variable is referenced, the JavaScript engine performs a scope chain lookup: it starts at the current scope's environment record and walks up through enclosing records until the variable is found or the global scope is exhausted.
V8 optimises this through:
Performance implication: deeply nested closures that close over many large variables create long-lived heap contexts. Flattening the closure chain or copying only the needed values can improve GC throughput.
Example:
// Unnecessarily deep scope chain — engine must walk up 3 levels on each access
function level1() {
const config = loadHeavyConfig(); // heap-allocated context (closed over)
function level2() {
function level3() {
return config.timeout; // 3-level chain lookup
}
return level3;
}
return level2;
}
// Flatten: extract only needed value to the immediate scope
function level1Optimised() {
const { timeout } = loadHeavyConfig(); // only close over what is needed
function level2() {
function level3() {
return timeout; // 1 var, still 3-level chain but smaller context object
}
return level3;
}
return level2;
}
Real-World Use Case:
In Node.js request handlers that process millions of requests per minute (e.g., an Express.js API gateway), middleware functions close over the app config object. Profiling with --prof (V8 tick profiler) or Chrome DevTools' flame graph reveals when heap contexts from closures apply GC pressure. The optimisation is to destructure only required config fields at the module level — keeping the closure context objects small and promoting faster minor GC cycles.
this) in JavaScript?These two concepts are frequently confused but are entirely independent mechanisms:
| Scope | Context (this) | |
|---|---|---|
| Determined by | Where code is written (lexical) | How a function is called (dynamic) |
| Stores | Variable bindings | The object a function is operating upon |
| Changed by | Impossible at runtime | call, apply, bind, arrow functions, class fields |
| Applies to | All identifiers | Only the this keyword |
Arrow functions are the intersection: they do not create their own this binding — they inherit this lexically from the enclosing scope at definition time. This makes them behave like closures for this.
Example:
const timer = {
label: 'Countdown',
seconds: 3,
// Regular function: `this` is determined by caller (lost in setTimeout callback)
startBroken() {
setTimeout(function () {
console.log(this.label); // undefined — `this` is global/undefined (strict mode)
}, 1000);
},
// Arrow function: `this` inherited lexically from startFixed\'s scope
startFixed() {
setTimeout(() => {
console.log(this.label); // 'Countdown'
}, 1000);
},
// Explicit bind alternative
startWithBind() {
setTimeout(function () {
console.log(this.label); // 'Countdown'
}.bind(this), 1000);
},
};
timer.startFixed();
Real-World Use Case:
React class components historically required .bind(this) in the constructor for event handlers because a regular function passed as a JSX prop loses its this binding. The shift to arrow function class fields (handleClick = () => { ... }) and then to functional components with hooks eliminated this entire category of this-related bugs — demonstrating that understanding the scope vs. context distinction is foundational to making informed decisions about component architecture.
Hoisting is a JavaScript mechanism where variable, function, class, and import declarations are conceptually moved to the top of their containing scope during the compilation phase, before the code executes. Hoisting allows functions to be safely used in code before they are declared.
Example 01: Variable Hoisting
console.log(message); // output: undefined
var message = "The variable Has been hoisted";
The above code looks like as below to the interpreter,
var message;
console.log(message);
message = "The variable Has been hoisted";
Example 02: let and const hoisting
All declarations (function, var, let, const and class) are hoisted in JavaScript, while the var declarations are initialized with undefined, but let and const declarations remain uninitialized.
console.log(x);
let x = 10;
// Output: ReferenceError: x is not defined
Example 03: Function Hoisting
One of the advantages of hoisting is that it lets you use a function before you declare it in your code.
getName("Sadhika Sandal");
function getName(name) {
console.log("Hello " + name);
}
Output:
Hello Sadhika Sandal
They will only get initialized when their lexical binding (assignment) is evaluated during runtime by the JavaScript engine. This means we can't access the variable before the engine evaluates its value at the place it was declared in the source code. This is what we call Temporal Dead Zone (TDZ), A time span between variable creation and its initialization where they can't be accessed.
What gets hoisted and how:
| Declaration | Hoisted? | Initialised to |
|---|---|---|
var | Yes | undefined |
let | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
const | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
function declaration | Yes | Full function body |
function expression (var f = function(){}) | Partially | var f hoisted as undefined; body is not hoisted |
class declaration | Yes (TDZ) | uninitialised — ReferenceError if accessed before declaration |
import | Yes | Fully evaluated before module body runs |
Real-World Use Case:
Node.js module systems and build tools like Webpack rely on function-declaration hoisting to allow modules to call utility functions defined later in the same file — enabling a readable "public API at the top, implementation details below" code style. However, misunderstanding var hoisting is responsible for a large class of production bugs in legacy jQuery/Angular 1 codebases, which is why the Airbnb ESLint config bans var and mandates let/const.
Let us take the following function expression
var foo = function foo() {
return 12;
}
In JavaScript var-declared variables and functions are hoisted. Let us take function hoisting first. Basically, the JavaScript interpreter looks ahead to find all the variable declaration and hoists them to the top of the function where It is declared. For example:
foo(); // Here foo is still undefined
var foo = function foo() {
return 12;
};
The code above behind the scene look something like this:
var foo = undefined;
foo(); // Here foo is undefined
foo = function foo() {
// Some code stuff
}
var foo = undefined;
foo = function foo() {
// Some code stuff
}
foo(); // Now foo is defined here
function foo() {}): The entire function — name and body — is hoisted. The function is callable anywhere in its scope, even before the line it appears on.const foo = function() {}): Only the variable declaration is hoisted (if var); the assignment — the function value itself — is not. Calling it before the line throws TypeError (not ReferenceError) because the variable exists but holds undefined.const foo = () => {}): Same as function expression — it is always assigned to a variable, so the same rules apply.Example:
// 1. Function Declaration — fully hoisted
console.log(add(2, 3)); // 5
function add(a, b) {
return a + b;
}
// 2. Function Expression with var — var hoisted as undefined
console.log(multiply); // undefined
// console.log(multiply(2, 3)); // TypeError: multiply is not a function
var multiply = function (a, b) {
return a * b;
};
// 3. Function Expression with let — TDZ
// console.log(divide(10, 2)); // ReferenceError
const divide = (a, b) => a / b;
// 4. Practical gotcha: conditional function declaration (avoid — browser-dependent)
if (true) {
function conditionalFn() { return 'inside if'; }
}
// Behaviour in strict mode: conditionalFn is block-scoped to the if-block
// Behaviour in sloppy mode: varies by browser — avoid this pattern entirely
Real-World Use Case:
In large React component files, developers often define helper arrow functions (const formatDate = (d) => ...) after the component function that uses them. Because the component is a const arrow function itself, calling formatDate inside it only works if formatDate is defined above — otherwise a ReferenceError occurs at module load time. Understanding hoisting rules guides file organisation: place utility declarations before their consumers, or use function declarations when ordering flexibility matters.
Class declarations are hoisted to the top of their block scope — like let and const — but they are not initialised until the declaration is evaluated. This means they sit in the Temporal Dead Zone (TDZ) and accessing a class before its declaration throws a ReferenceError, not a TypeError. This is a deliberate choice to prevent confusing inheritance hierarchies from being constructed before base classes are fully defined.
Example:
// Class in TDZ — accessing before declaration throws ReferenceError
// const p = new Person('Alice'); // ReferenceError: Cannot access 'Person' before initialization
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, I am ${this.name}`;
}
}
const p = new Person('Alice'); //
console.log(p.greet()); // 'Hi, I am Alice'
// Class expression — same as function expression: name is in TDZ if const/let
const Animal = class {
constructor(species) {
this.species = species;
}
};
const dog = new Animal('Canis lupus'); //
Practical implication — class vs. function constructor:
// Function constructor: hoisted entirely — can be used before declaration
const f = new Foo(); //
function Foo() { this.x = 1; }
// Class: TDZ — cannot be used before declaration
// const b = new Bar(); // ReferenceError
class Bar { constructor() { this.x = 1; } }
Real-World Use Case:
When splitting large Node.js services into files with circular require() references, class-based modules that are accessed during the require() cycle before the class declaration evaluates throw ReferenceError. Understanding the TDZ for classes helps engineers design module load order carefully — or avoid circular dependencies by refactoring to dependency injection patterns (e.g., passing the class as a parameter rather than importing it at the module level).
let/const blocks, and how does it affect switch statements?let and const are block-scoped and hoisted within their block to the TDZ. A switch statement's {} is a single block — all case clauses share the same block scope. This causes a subtle bug: a let declared in one case is technically in scope (hoisted) for all other cases in the same switch block, causing a SyntaxError if another case attempts to declare the same name.
Example:
// SyntaxError: Identifier 'result' has already been declared
function getLabel(status) {
switch (status) {
case 'active':
let result = 'User is active'; // hoisted to top of switch block
return result;
case 'inactive':
let result = 'User is inactive'; // SyntaxError — same block, same name
return result;
}
}
// Fix 1: wrap each case in its own block
function getLabelFixed(status) {
switch (status) {
case 'active': {
let result = 'User is active';
return result;
}
case 'inactive': {
let result = 'User is inactive'; // own block — no conflict
return result;
}
default:
return 'Unknown';
}
}
// Fix 2: prefer object lookup over switch for simple mappings
const STATUS_LABELS = {
active: 'User is active',
inactive: 'User is inactive',
};
const getLabel2 = (status) => STATUS_LABELS[status] ?? 'Unknown';
Real-World Use Case:
This exact bug has appeared in production Redux reducers where developers switched from var to let during a linting migration. All case branches in a reducer's switch share one block, so re-declaring let draft in multiple cases causes a SyntaxError caught only at parse time — blocking the entire app from loading. The standard Redux Toolkit solution is to use Immer-based createSlice instead, which replaces the switch reducer pattern entirely.
var declarations and function declarations with the same name?When multiple var declarations or function declarations share the same identifier in the same scope, the engine applies specific precedence rules during the compilation phase:
var declarations are merged into one — only one undefined slot is allocated; whichever assignment executes last wins at runtime.var declarations of the same name during hoisting — the function body wins over var's undefined.Example:
// What does the programmer see:
var x = 1;
function x() { return 'fn'; }
var x = 2;
// What the engine processes during compilation:
// 1. var x → undefined (registered)
// 2. function x → x = function() { return 'fn'; } (overwrites var)
// 3. (execution phase) x = 1
// 4. (execution phase) x = 2
console.log(x); // 2 — last assignment wins
// Multiple function declarations — last one wins
function greet() { return 'Hello'; }
function greet() { return 'Hi'; } // overwrites the first
console.log(greet()); // 'Hi'
// var + function same name — function hoisted OVER var during compilation
console.log(typeof answer); // 'function' (not 'undefined')
var answer = 42;
function answer() { return 'forty-two'; }
console.log(answer); // 42 — runtime assignment overrides the function
Why this matters architecturally:
In large legacy codebases (pre-modules), concatenating multiple script files together frequently caused silent name collisions of this kind. Two files each declaring var config = ... or function init() {} would silently overwrite each other. The last file in the bundle always won, causing intermittent and environment-specific bugs that only appeared in production where file concatenation order differed from the development setup.
Real-World Use Case:
This collision class of bugs was a primary driver for the adoption of the Module Pattern (IIFE-based), then AMD/CommonJS, and ultimately ES modules — each solution adding a layer of scope isolation to prevent hoisting-based name collisions. Modern bundlers (Webpack, Rollup) assign each module its own scope by wrapping it, making cross-module var collision impossible. Understanding this history explains why "use strict" and ES modules are defaults in all modern toolchains.
Arrow functions (=>) introduced in ES2015 are a concise syntax for writing functions, but they are not simply shorthand for function. They have fundamental semantic differences:
| Feature | Regular Function | Arrow Function |
|---|---|---|
this binding | Dynamic — determined by call site | Lexical — inherits from enclosing scope |
arguments object | Available | Not available (use rest params) |
new (constructor) | Can be used | Throws TypeError |
prototype property | Has one | undefined |
super / new.target | Own binding | Inherited from enclosing scope |
| Method shorthand | Usable (with caution) | Avoid for object methods needing this |
When NOT to use arrow functions:
this (obj.method() — arrow's this is the enclosing scope, not obj).new.this to refer to the DOM element (element.addEventListener('click', () => {}) — this is the outer context, not the element).function*.arguments object.// --- Concise bodies ---
const double = x => x * 2; // single param, expression body
const add = (a, b) => a + b; // multiple params
const toUpper = str => ({ upper: str.toUpperCase() }); // returning an object literal — wrap in ()
// --- `this` lexical binding ---
class Timer {
constructor() {
this.ticks = 0;
}
start() {
// Arrow function: `this` is the Timer instance (lexical)
this.interval = setInterval(() => {
this.ticks++;
console.log(this.ticks);
}, 1000);
}
stop() {
clearInterval(this.interval);
}
}
// --- When NOT to use arrow ---
const counter = {
count: 0,
// Arrow: `this` is the module/global scope, not counter
incrementBad: () => { counter.count++; },
// Method shorthand: `this` is counter
incrementGood() { this.count++; },
};
// Arrow as constructor
// const p = new (() => {}); // TypeError: (intermediate value) is not a constructor
// No own `arguments`
const fn = () => {
// console.log(arguments); // ReferenceError in strict mode / module scope
};
const fn2 = (...args) => args; // Use rest params instead
Real-World Use Case:
React's functional components and hooks use arrow functions almost exclusively for event handlers passed as JSX props (onClick={() => dispatch(action)}). Because these arrow functions inherit this from the component scope (which in a functional component is irrelevant — there is no this), the lexical binding is harmless and the concise syntax reduces boilerplate. Conversely, in Express.js route handlers or Mongoose middleware that rely on this referring to the document/request, using arrow functions is a common production bug — the official Mongoose docs explicitly warn against it.
Template literals (backtick strings) introduced in ES2015 offer four key capabilities over traditional string literals:
${expr}.\n needed.${} including ternaries, function calls, and arithmetic.// 1. Interpolation
const user = { name: 'Alice', score: 42 };
console.log(`Player: ${user.name}, Score: ${user.score * 2}`); // Player: Alice, Score: 84
// 2. Multi-line
const html = `
<article>
<h1>${user.name}</h1>
<p>Score: ${user.score}</p>
</article>
`.trim();
// 3. Expressions inside ${}
const items = ['apple', 'banana', 'cherry'];
console.log(`You have ${items.length} item${items.length !== 1 ? 's' : ''}.`);
// 'You have 3 items.'
// 4. Tagged template literals
function highlight(strings, ...values) {
return strings.reduce((result, str, i) =>
result + str + (values[i] !== undefined ? `<mark>${values[i]}</mark>` : ''),
''
);
}
const product = 'JavaScript';
const year = 2015;
console.log(highlight`${product} was standardised in ${year}.`);
// '<mark>JavaScript</mark> was standardised in <mark>2015</mark>.'
// SQL injection prevention with tagged templates (concept)
function sql(strings, ...values) {
const sanitised = values.map(v => sanitiseInput(v)); // escape each interpolated value
return strings.reduce((q, str, i) => q + str + (sanitised[i] ?? ''), '');
}
function sanitiseInput(v) { return String(v).replace(/'/g, "''"); }
const userId = "1' OR '1'='1"; // SQL injection attempt
const query = sql`SELECT * FROM users WHERE id = '${userId}'`;
// SELECT * FROM users WHERE id = '1'' OR ''1''=''1' — safely escaped
Real-World Use Case:
The styled-components and emotion CSS-in-JS libraries are built entirely on tagged template literals. When you write styled.div`color: ${props => props.primary ? 'blue' : 'grey'};`, the library's tag function receives the static CSS parts and the dynamic value functions, generates a unique class name, injects a <style> rule, and tracks theme changes — all without any custom parser. This demonstrates that tagged templates can implement fully featured DSLs (CSS, GraphQL, SQL, i18n) natively in JavaScript with zero dependencies.
Destructuring is a syntax for unpacking values from arrays or properties from objects into distinct variables in a single statement. It is purely syntactic sugar — the engine converts it to a series of property accesses and assignments.
// ── Object destructuring ──────────────────────────────────────────────────────
const user = { id: 1, name: 'Alice', role: 'admin', address: { city: 'NYC', zip: '10001' } };
// Basic
const { id, name } = user;
// Rename (alias)
const { name: username, role: userRole } = user;
console.log(username, userRole); // 'Alice', 'admin'
// Default values (used when property is undefined)
const { theme = 'light', lang = 'en' } = user;
console.log(theme, lang); // 'light', 'en'
// Nested destructuring
const { address: { city, zip } } = user;
console.log(city, zip); // 'NYC', '10001'
// Rest in object destructuring
const { id: userId, ...rest } = user;
console.log(userId); // 1
console.log(rest); // { name: 'Alice', role: 'admin', address: {...} }
// ── Array destructuring ───────────────────────────────────────────────────────
const rgb = [255, 128, 0];
const [red, green, blue] = rgb;
const [r, , b] = rgb; // skip elements with empty commas
const [first, ...others] = rgb; // rest in array
// Swap variables without temp
let a = 1, bVal = 2;
[a, bVal] = [bVal, a];
console.log(a, bVal); // 2, 1
// Default values
const [x = 0, y = 0, z = 0] = [10, 20];
console.log(x, y, z); // 10, 20, 0
// ── Function parameter destructuring ─────────────────────────────────────────
function renderCard({ title, subtitle = 'No subtitle', tags = [] } = {}) {
return `<h1>${title}</h1><h2>${subtitle}</h2><ul>${tags.map(t => `<li>${t}</li>`).join('')}</ul>`;
}
renderCard({ title: 'Hello', tags: ['js', 'es6'] });
renderCard(); // uses defaults — the `= {}` prevents TypeError when called with no args
// ── Mixed / deeply nested ─────────────────────────────────────────────────────
const apiResponse = {
data: {
users: [
{ id: 1, profile: { avatar: 'alice.png' } },
{ id: 2, profile: { avatar: 'bob.png' } },
],
},
};
const { data: { users: [{ id: firstId, profile: { avatar: firstAvatar } }] } } = apiResponse;
console.log(firstId, firstAvatar); // 1, 'alice.png'
Real-World Use Case:
React's useState hook returns a two-element array precisely to enable array destructuring with custom naming: const [count, setCount] = useState(0). This pattern lets each call site choose its own variable names without the library needing to know them. Similarly, React component props are universally destructured in the function signature (function Button({ label, onClick, disabled = false }) {}), reducing prop-drilling verbosity and making prop contracts immediately visible — a pattern enforced by most team style guides and ESLint configs.
Both use the ... syntax but serve opposite purposes:
Rest (...params) | Spread (...iterable) | |
|---|---|---|
| Position | Last parameter in a function signature | Call site, array literal, or object literal |
| Purpose | Collects multiple arguments into an array | Expands an iterable/object into individual elements |
| Result | One Array | Multiple values |
// ── Rest parameter ───────────────────────────────────────────────────────────
function sum(...numbers) { // collects all args into an array
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
function log(level, ...messages) { // first arg fixed, rest collected
console[level](messages.join(' '));
}
log('warn', 'Disk', 'usage', 'high'); // console.warn('Disk usage high')
// ── Spread operator ──────────────────────────────────────────────────────────
// 1. Expanding arrays
const nums = [1, 2, 3];
const more = [...nums, 4, 5]; // [1, 2, 3, 4, 5]
const copy = [...nums]; // shallow copy
// 2. Spreading into a function call
console.log(Math.max(...nums)); // 3
// 3. Merging / cloning objects (shallow)
const defaults = { theme: 'light', lang: 'en', debug: false };
const userPrefs = { theme: 'dark', fontSize: 14 };
const config = { ...defaults, ...userPrefs }; // right side wins on key collision
// { theme: 'dark', lang: 'en', debug: false, fontSize: 14 }
// 4. Spreading a string into characters
const chars = [..."hello"]; // ['h', 'e', 'l', 'l', 'o']
// 5. Converting NodeList / Set to Array
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]
Real-World Use Case:
Redux reducers use object spread to produce new state immutably without mutating the existing state object — a requirement of Redux's change-detection mechanism: return { ...state, isLoading: false, data: action.payload }. This replaces Object.assign({}, state, { isLoading: false }) with far more readable syntax. The V8 engine optimises spread-based object cloning with the same hidden-class mechanisms as Object.assign, so there is no meaningful performance difference in most application code.
Map and Set — how do they differ from plain objects and arrays, and when should you use them?Set is a collection of unique values of any type. It maintains insertion order and provides O(1) has() lookup (hash-based internally in V8).
Map is a collection of key-value pairs where keys can be any type (including objects, functions, and NaN). Plain objects only allow string/Symbol keys.
| Feature | Object | Map |
|---|---|---|
| Key types | String / Symbol only | Any value (including objects) |
| Ordered | Not guaranteed (though V8 preserves insertion order for string keys in practice) | Insertion order guaranteed by spec |
| Size | Object.keys(obj).length — O(n) | map.size — O(1) |
| Iteration | for...in (includes prototype) / Object.entries() | for...of, forEach, direct iterators |
| Prototype pollution | Possible (e.g., __proto__) | Not possible |
| JSON serialisable | Yes | (needs custom serialiser) |
| Feature | Array | Set |
|---|---|---|
| Uniqueness | No — duplicates allowed | Yes — duplicates silently ignored |
has / membership | indexOf / includes — O(n) | set.has() — O(1) |
| Ordered | Index-based | Insertion-order |
// ── Set ──────────────────────────────────────────────────────────────────────
const visited = new Set();
visited.add('/home');
visited.add('/about');
visited.add('/home'); // duplicate — silently ignored
console.log(visited.size); // 2
console.log(visited.has('/about')); // true (O(1))
// Deduplicate an array
const tags = ['js', 'css', 'js', 'html', 'css'];
const uniqueTags = [...new Set(tags)]; // ['js', 'css', 'html']
// ── Map ──────────────────────────────────────────────────────────────────────
const cache = new Map();
// Object as key (impossible with plain objects as keys)
const req1 = { url: '/api/users' };
const req2 = { url: '/api/posts' };
cache.set(req1, { data: [1, 2, 3], ts: Date.now() });
cache.set(req2, { data: ['a', 'b'], ts: Date.now() });
console.log(cache.get(req1).data); // [1, 2, 3]
console.log(cache.size); // 2
// Iteration
for (const [key, value] of cache) {
console.log(key.url, value.data);
}
// Convert to / from plain object
const mapFromObj = new Map(Object.entries({ a: 1, b: 2 }));
const objFromMap = Object.fromEntries(mapFromObj);
Real-World Use Case:
WeakMap (a variant of Map with weak references and object-only keys) is used by React's Concurrent Mode internals to associate component fiber metadata with DOM nodes without preventing garbage collection of unmounted components. Set is used in many state management libraries (Zustand, Jotai) to track subscriber callbacks: O(1) add/delete/has operations make subscription management significantly faster than array-based alternatives at scale (thousands of subscribers in large dashboards).
undefined vs. null?ES6 default parameters allow function parameters to have a fallback value used when the argument is undefined or when no argument is provided. Crucially, passing null does not trigger the default — null is an explicit value meaning "no object here", while undefined means "value not provided".
Default expressions are evaluated lazily — they run each time the function is called with a missing argument, not once at function definition time. This allows defaults that reference other parameters or call functions.
// Basic defaults
function createUser(name, role = 'viewer', createdAt = new Date()) {
return { name, role, createdAt };
}
createUser('Alice'); // role: 'viewer', createdAt: <now>
createUser('Bob', 'admin'); // role: 'admin'
createUser('Carol', undefined, new Date('2024-01-01')); // role: 'viewer' (undefined triggers default)
createUser('Dave', null); // role: null — null does NOT trigger default
// Default referencing earlier parameter
function range(start, end = start + 10) {
return Array.from({ length: end - start }, (_, i) => start + i);
}
console.log(range(5)); // [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
console.log(range(1, 4)); // [1, 2, 3]
// Default calling a function (evaluated per call)
let callCount = 0;
function getDefault() { return ++callCount; }
function track(id = getDefault()) {
Truncated — view the full README on GitHub.
339 followers · starred Feb 2022
248 followers · starred Apr 2021
19 followers · starred Aug 2024
168 followers · starred Aug 2021