Various Code Examples And Patterns
call, bind, and apply
JavaScript’s call()
, bind()
, and apply()
methods are powerful tools for controlling function context (this
) at runtime.
They are especially useful when borrowing methods, delaying execution, or separating data from behavior.
call()
immediately invokes the function with a specifiedthis
context and arguments.apply()
is similar tocall()
, but it takes arguments as an array.bind()
returns a new function with a permanently bound context.
These methods are commonly used when:
- Reusing methods across different objects
- Passing methods as callbacks without losing context
- Implementing decorators or delayed invocation
- Writing functional utilities like
rateLimit
This section also demonstrates how arrow functions behave differently with this
, and includes a custom implementation of Function.prototype.bind
.
Example: Losing and Rebinding Context
This example demonstrates what happens when a method loses its context (this
) and how to recover it using call()
and bind()
:
- Calling
counter.add(1)
works as expected —this
refers tocounter
. - When passing
counter.add
directly to another function (operate
), it loses context and throws an error. - Using
call()
explicitly sets the context. - Using
bind()
returns a new function withthis
permanently bound tocounter
.
const counter = {
value: 10,
add(num) {
this.value += num;
return this.value;
},
};
function operate(method, num) {
return method(num);
}
function operateWithCall(method, num) {
return method.call(counter, num);
}
console.log(counter.add(1)); // 11
console.log(operateWithCall(counter.add, 1)); // 12
console.log(operate(counter.add.bind(counter), 1)); // 13
try {
console.log(operate(counter.add, 1));
} catch (e) {
console.error(e.message);
// Cannot read properties of undefined (reading 'value')
}
Example 1: Using call
and apply
to Change Context
This example shows how call()
and apply()
can be used to invoke a method with a different this
context:
objA.print.call(objB)
executesobjA.print
withobjB
asthis
, so it logs"I am from objB"
.apply()
works similarly, but takes arguments as an array.
const objA = {
data: "I am from objA",
print() {
console.log(this.data);
},
};
const objB = {
data: "I am from objB",
};
objA.print.call(objB); // I am from objB
objA.print.apply(objB); // I am from objB
Example 2: Preserving Method Context with setTimeout
This example shows how to preserve the this
context when calling a method asynchronously using setTimeout
.
The delayMethod
function returns a wrapper that defers method execution by 1 second while ensuring that this
still refers to the original object.
user.greet()
usesthis.name
, so it’s crucial to call it with the correct context.- Wrapping it with
obj[methodName](...args)
inside the timeout maintains the correctthis
.
function delayMethod(obj, methodName) {
return function (...args) {
setTimeout(() => obj[methodName](...args), 1000);
};
}
const user = {
name: "Alice",
greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
},
};
const delayedGreet = delayMethod(user, "greet");
delayedGreet("Hello"); // After 1 second: "Hello, Alice!"
Example 3: Custom bind()
Implementation
This example demonstrates how to implement a simplified version of Function.prototype.bind()
.
The customBind
function:
- Stores a reference to the original function.
- Returns a new function that, when called, invokes the original with a fixed
this
context and any partially applied arguments.
This mimics native bind()
behavior, including partial application of arguments.
Function.prototype.customBind = function (context, ...args) {
const originalFunction = this;
return function (...newArgs) {
return originalFunction.apply(context, [...args, ...newArgs]);
};
};
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: "John" };
const boundGreet = greet.customBind(person, "Hello");
boundGreet("!"); // "Hello, John!"
Example 4: Sharing Methods Across Objects with call()
In this example, the calculateTotal
method from store1
is reused for store2
by using Function.prototype.call()
.
call()
setsthis
tostore2
, so the method computes tax usingstore2.taxRate
.- As an alternative, the method is directly assigned to
store2
, allowing normal invocation syntax.
const store1 = {
taxRate: 0.1,
calculateTotal(price, quantity) {
const subtotal = price * quantity;
return subtotal + subtotal * this.taxRate;
},
};
const store2 = {
taxRate: 0.2,
};
const price = 50;
const quantity = 2;
// Calculate total for `store2` using the method from `store1`
const total = store1.calculateTotal.call(store2, price, quantity);
console.log(total); // 120 (price * quantity + tax)
// 2 solution
store2.calculateTotal = store1.calculateTotal;
console.log(store2.calculateTotal(price, quantity));
Example 5: bind()
for Context in Rate-Limited Function
This example demonstrates how to use bind()
to ensure the correct this
context when passing an object's method to a wrapper function.
logger.log
is passed intorateLimit()
which returns a throttled version of the method.- Since
log
relies onthis.message
,bind(logger)
is required to preserve the context. - Without
bind()
,this.message
would beundefined
whenlog()
is called inside the throttled wrapper.
function rateLimit(fn, ms) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall < ms) return;
lastCall = now;
return fn(...args);
};
}
const logger = {
message: "Rate limited log",
log() {
console.log(this.message);
},
};
const rateLimitedLog = rateLimit(logger.log.bind(logger), 1000);
rateLimitedLog(); // Rate limited log
rateLimitedLog();
setTimeout(rateLimitedLog, 1500);
// Logs "Rate limited log" after 1.5s
Example 6: Using bind()
to Preserve this
In this example, greet()
is a regular function that relies on this.name
. When it's not called in the context of an object, this
would be undefined
.
To ensure the correct context (person
), we create a bound version of the function using bind(person)
.
This guarantees that this.name
inside greet()
always refers to "John"
, even if called independently.
const person = {
name: "John",
};
function greet() {
console.log(`Hello, ${this.name}`);
}
// Create a bound function
const boundGreet = greet.bind(person);
boundGreet(); // Hello, John
Example 7: Arrow Functions Ignore call
In this example, the arrow
function is defined inside the greet()
method using an arrow function.
Arrow functions do not have their own this
— they inherit this
from their enclosing lexical context.
So even when using call()
with a different this
value ({ name: "Bob" }
), the arrow function still uses
this.name
from the outer greet()
context, which is user
.
Thus, the output is: Hello, Alice
.
const user = {
name: "Alice",
greet() {
const arrow = () => console.log(`Hello, ${this.name}`);
arrow.call({ name: "Bob" }); // Arrow functions ignore `call`
},
};
user.greet(); // Hello, Alice
Example 8: Preserving this
in Callbacks
When passing a method like user.logName
to another function (e.g. executeCallback
), the this
context is lost unless it is explicitly preserved.
bind(user)
ensures thatthis
always refers touser
.- An arrow function
() => user.logName()
also preserves the context by calling the method directly onuser
.
Both approaches ensure the correct this
context, so the output is: Alice
.
const user = {
name: "Alice",
logName() {
console.log(this.name);
},
};
function executeCallback(callback) {
callback();
}
// Preserve context with `bind`
executeCallback(user.logName.bind(user)); // Alice
executeCallback(() => user.logName()); // Alice
clsx
The clsx
utility is commonly used to conditionally join class names in JavaScript and React projects.
This custom implementation mimics the behavior of the clsx
library by:
- Skipping falsy values (
false
,null
,undefined
,0
,""
,NaN
) - Joining strings as-is
- Recursively flattening arrays
- Including keys from objects whose values are truthy
This allows for clean, readable class name composition in UI code.
function clsx(...args) {
const classes = [];
for (const arg of args) {
// Skip the current iteration if the argument is falsy
if (!arg) continue;
if (typeof arg === "string") {
classes.push(arg);
} else if (Array.isArray(arg)) {
classes.push(clsx(...arg)); // Recursively process arrays
} else if (typeof arg === "object") {
for (const key in arg) {
if (arg[key]) {
classes.push(key); // Push key if value is truthy
}
}
}
}
return classes.join(" "); // Join classes with a space
}
console.log(
clsx("base-class", { active: true, disabled: false }, [
"additional-class",
"another-class",
]),
); // base-class active additional-class another-class
console.log(
clsx(null, false, "bar", undefined, { baz: null }, "", [[[{ one: 1 }]]]),
); // bar one
contains duplicate
This function checks if a given array contains any duplicate values.
It uses a Set
to track elements that have already been seen while iterating through the array.
If a value is found in the Set
, the function returns true
, indicating a duplicate.
If no duplicates are found by the end of iteration, it returns false
.
function containsDuplicate(nums) {
const seen = new Set();
for (const num of nums) {
if (seen.has(num)) {
return true;
}
seen.add(num);
}
return false;
}
console.log(containsDuplicate([1, 1, 3, 4])); // true
console.log(containsDuplicate([1, 3, 4])); // false
deep clone
The deepClone
function recursively copies all levels of an object or array, creating a fully independent clone.
This avoids shared references and ensures that changes to nested structures in the cloned object won’t affect the original.
It handles:
- Primitive types and
null
- Arrays using recursive
map
- Plain objects using recursive property traversal
Perfect for deep copying JSON-compatible data without using JSON.parse(JSON.stringify(...))
,
which fails on special types like Date
, Map
, Set
, or circular references.
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (Array.isArray(obj)) {
// Recursively clone array elements
return obj.map(deepClone);
}
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// Recursively clone object properties
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
const obj = { a: 1, b: [{ c: 2, d: 3 }] };
const clonedObj = deepClone(obj);
console.log(clonedObj); // { a: 1, b: [ { c: 2, d: 3 } ] }
dictionary of nested arrays
This example demonstrates how to transform a deeply nested array structure (e.g., categories → subcategories → items)
into a dictionary (object-based) format for fast lookup by id
.
Benefits:
- Constant-time access to categories, subcategories, and items by their
id
. - Improves performance for large datasets with frequent lookups.
- Easy to traverse and mutate without relying on nested
find()
calls.
Approaches Covered:
- Plain JS implementation using nested loops to assign items into objects keyed by
id
. - Recursive
mapToDictionary
utility that supports arbitrary nesting levels via akeys
argument. - TypeScript version with strict typing and reusable helper functions for building deeply nested dictionaries.
Great for building normalized state for UIs, lookup tables, or improving nested data traversal.
const data = [
{
id: 1,
name: "Category A",
items: [
{
id: 2,
name: "Subcategory A1",
items: [
{ id: 3, name: "Item A1-1", value: 10 },
{ id: 4, name: "Item A1-2", value: 15 },
],
},
{
id: 5,
name: "Subcategory A2",
items: [
{ id: 6, name: "Item A2-1", value: 20 },
{ id: 7, name: "Item A2-2", value: 25 },
],
},
],
},
{
id: 8,
name: "Category B",
items: [
{
id: 9,
name: "Subcategory B1",
items: [
{ id: 10, name: "Item B1-1", value: 30 },
{ id: 11, name: "Item B1-2", value: 35 },
],
},
{
id: 12,
name: "Subcategory B2",
items: [
{ id: 13, name: "Item B2-1", value: 40 },
{ id: 14, name: "Item B2-2", value: 45 },
],
},
],
},
];
function createNestedDictionary(data) {
const dictionary = {};
for (const category of data) {
dictionary[category.id] = { ...category, subcategories: {} };
for (const subcategory of category.items) {
dictionary[category.id].subcategories[subcategory.id] = {
...subcategory,
items: {},
};
for (const item of subcategory.items) {
dictionary[category.id].subcategories[subcategory.id].items[item.id] =
item;
}
}
}
return dictionary;
}
const nestedDictionary = createNestedDictionary(data);
console.log(nestedDictionary[1].name); // Category A
console.log(nestedDictionary[1].subcategories[5].name); // Subcategory A2
console.log(nestedDictionary[1].subcategories[5].items[7].name); // Item A2-2
console.log(nestedDictionary[8].subcategories[12].items[14]);
// { id: 14, name: 'Item B2-2', value: 45 }
const data = [
{
id: 1,
name: "Category A",
items: [
{
id: 2,
name: "Subcategory A1",
items: [
{ id: 3, name: "Item A1-1", value: 10 },
{ id: 4, name: "Item A1-2", value: 15 },
],
},
{
id: 5,
name: "Subcategory A2",
items: [
{ id: 6, name: "Item A2-1", value: 20 },
{ id: 7, name: "Item A2-2", value: 25 },
],
},
],
},
{
id: 8,
name: "Category B",
items: [
{
id: 9,
name: "Subcategory B1",
items: [
{ id: 10, name: "Item B1-1", value: 30 },
{ id: 11, name: "Item B1-2", value: 35 },
],
},
{
id: 12,
name: "Subcategory B2",
items: [
{ id: 13, name: "Item B2-1", value: 40 },
{ id: 14, name: "Item B2-2", value: 45 },
],
},
],
},
];
function mapToDictionary(data, keys) {
const [currentKey, ...remainingKeys] = keys;
return data.reduce((acc, item) => {
acc[item.id] = {
...item,
[currentKey || "items"]: item.items
? mapToDictionary(item.items, remainingKeys)
: undefined,
};
return acc;
}, {});
}
const nestedDictionary = mapToDictionary(data, ["subcategories", "items"]);
console.log(nestedDictionary[1].name); // Category A
console.log(nestedDictionary[1].subcategories[5].name); // Subcategory A2
console.log(nestedDictionary[1].subcategories[5].items[7].name); // Item A2-2
console.log(nestedDictionary[8].subcategories[9].items[11]);
// { id: 11, name: 'Item B1-2', value: 35, items: undefined }
typescript
type Item = {
id: number;
name: string;
value: number;
};
type Subcategory = {
id: number;
name: string;
items: Item[];
};
type Category = {
id: number;
name: string;
items: Subcategory[];
};
type Subcategories = Record<
number,
{
name: string;
items: Record<number, Item>;
}
>;
type NestedDictionary = Record<
number,
{
name: string;
subcategories: Subcategories;
}
>;
function mapItemsToDictionary(items: Item[]) {
return items.reduce(
(acc, item) => {
acc[item.id] = item;
return acc;
},
{} as Record<number, Item>,
);
}
function mapSubcategoriesToDictionary(
subcategories: Subcategory[],
) {
return subcategories.reduce((acc, subcategory) => {
acc[subcategory.id] = {
name: subcategory.name,
items: mapItemsToDictionary(subcategory.items),
};
return acc;
}, {} as Subcategories);
}
function createNestedDictionary(
categories: Category[],
): NestedDictionary {
return categories.reduce((acc, category) => {
acc[category.id] = {
name: category.name,
subcategories: mapSubcategoriesToDictionary(
category.items,
),
};
return acc;
}, {} as NestedDictionary);
}
const data: Category[] = [
{
id: 1,
name: "Category A",
items: [
{
id: 2,
name: "Subcategory A1",
items: [
{ id: 3, name: "Item A1-1", value: 10 },
{ id: 4, name: "Item A1-2", value: 15 },
],
},
{
id: 5,
name: "Subcategory A2",
items: [
{ id: 6, name: "Item A2-1", value: 20 },
{ id: 7, name: "Item A2-2", value: 25 },
],
},
],
},
{
id: 8,
name: "Category B",
items: [
{
id: 9,
name: "Subcategory B1",
items: [
{ id: 10, name: "Item B1-1", value: 30 },
{ id: 11, name: "Item B1-2", value: 35 },
],
},
{
id: 12,
name: "Subcategory B2",
items: [
{ id: 13, name: "Item B2-1", value: 40 },
{ id: 14, name: "Item B2-2", value: 45 },
],
},
],
},
];
const nestedDictionary = createNestedDictionary(data);
console.log(nestedDictionary[1].name); // Category A
console.log(nestedDictionary[1].subcategories[5].name); // Subcategory A2
console.log(
nestedDictionary[1].subcategories[5].items[7].name,
); // Item A2-2
console.log(
nestedDictionary[8].subcategories[12].items[14].value,
); // 45
filter by related property
This utility filters a list of objects based on a related property found in another array — similar to a join + filter operation.
In this example, we filter items in the objects
array based on the class
value of their related object_type
, which is matched against the object_types
array.
Highlights:
groupBy()
helps groupobject_types
byclass
for fast lookup.filterObjectsByClass()
efficiently filters items by matching related object type's class.- Uses TypeScript generics for reusability and type safety.
const objects = [
{ id: 1, name: "Test 1", object_type: 1 },
{ id: 2, name: "Test 2", object_type: 1 },
{ id: 3, name: "Test 3", object_type: 2 },
{ id: 4, name: "Test 4", object_type: 3 },
];
const object_types = [
{ id: 1, class: "orange" },
{ id: 2, class: "orange" },
{ id: 3, class: "apple" },
{ id: 4, class: "cheese" },
];
const groupBy = <T, K extends string | number | symbol>(
arr: T[],
callback: (item: T) => K,
): Record<K, T[]> => {
return arr.reduce(
(acc: Record<K, T[]>, item: T) => {
const key = callback(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
},
{} as Record<K, T[]>,
);
};
const filterObjectsByClass = <T>(
cls: string,
objects: (T & { object_type: number })[],
objectTypes: { id: number; class: string }[],
): T[] => {
const result: T[] = [];
const objTypesByClass = groupBy(
objectTypes,
(item) => item.class,
);
for (const item of objects) {
if (
objTypesByClass[cls].find(
(objectType) => objectType.id === item.object_type,
)
) {
result.push(item);
}
}
return result;
};
const filteredObjects = filterObjectsByClass(
"orange",
objects,
object_types,
);
console.log(filteredObjects);
// [
// { id: 1, name: 'Test 1', object_type: 1 },
// { id: 2, name: 'Test 2', object_type: 1 },
// { id: 3, name: 'Test 3', object_type: 2 }
// ]
filterMap
The filterMap()
utility combines filtering and mapping into a single reduce()
pass — making it more efficient than chaining .filter().map()
.
This function takes:
- an
array
, - a
filterBoolean
function to determine which elements to include, - and a
mapCallback
to transform each included item.
export const filterMap = (array, filterBoolean, mapCallback) => {
return array.reduce((acc, item, idx) => {
if (filterBoolean(item)) {
acc.push(mapCallback(item, idx));
}
return acc;
}, []);
};
const people = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true },
];
const activeNames = filterMap(
people,
(person) => person.active,
(person) => person.name,
);
console.log(activeNames); // ['Alice', 'Charlie']
flat nested
Transforms deeply nested category-subcategory-item data into a flat array where each item contains metadata about its parent subcategory and category.
You can do it manually with loops or concisely with flatMap()
.
const data = [
{
id: 1,
name: "Category A",
items: [
{
id: 2,
name: "Subcategory A1",
items: [
{ id: 3, name: "Item A1-1", value: 10 },
{ id: 4, name: "Item A1-2", value: 15 },
],
},
{
id: 5,
name: "Subcategory A2",
items: [
{ id: 6, name: "Item A2-1", value: 20 },
{ id: 7, name: "Item A2-2", value: 25 },
],
},
],
},
{
id: 8,
name: "Category B",
items: [
{
id: 9,
name: "Subcategory B1",
items: [
{ id: 10, name: "Item B1-1", value: 30 },
{ id: 11, name: "Item B1-2", value: 35 },
],
},
{
id: 12,
name: "Subcategory B2",
items: [
{ id: 13, name: "Item B2-1", value: 40 },
{ id: 14, name: "Item B2-2", value: 45 },
],
},
],
},
];
function transformItems(data) {
const transformedItems = [];
for (const category of data) {
for (const subcategory of category.items) {
for (const item of subcategory.items) {
transformedItems.push({
id: item.id,
name: item.name,
value: item.value,
subcategory: subcategory.name,
category: category.name,
});
}
}
}
return transformedItems;
}
const itemsWithCategories = transformItems(data);
console.log(itemsWithCategories);
// [
// {
// id: 3,
// name: "Item A1-1",
// value: 10,
// subcategory: "Subcategory A1",
// category: "Category A",
// },
// {
// id: 4,
// name: "Item A1-2",
// value: 15,
// subcategory: "Subcategory A1",
// category: "Category A",
// },
// {
// id: 6,
// name: "Item A2-1",
// value: 20,
// subcategory: "Subcategory A2",
// category: "Category A",
// },
// {
// id: 7,
// name: "Item A2-2",
// value: 25,
// subcategory: "Subcategory A2",
// category: "Category A",
// },
// {
// id: 10,
// name: "Item B1-1",
// value: 30,
// subcategory: "Subcategory B1",
// category: "Category B",
// },
// {
// id: 11,
// name: "Item B1-2",
// value: 35,
// subcategory: "Subcategory B1",
// category: "Category B",
// },
// {
// id: 13,
// name: "Item B2-1",
// value: 40,
// subcategory: "Subcategory B2",
// category: "Category B",
// },
// {
// id: 14,
// name: "Item B2-2",
// value: 45,
// subcategory: "Subcategory B2",
// category: "Category B",
// },
// ];
function transformItems2(data) {
return data.flatMap((category) =>
category.items.flatMap((subcategory) =>
subcategory.items.map((item) => ({
id: item.id,
name: item.name,
value: item.value,
subcategory: subcategory.name,
category: category.name,
})),
),
);
}
const itemsWithCategories2 = transformItems2(data);
console.log(itemsWithCategories2);
// ...same result
group list by quarters
Groups monthly financial data by calendar quarters using the getQuarter()
method from
the date-fns
library. Each group includes individual monthly entries and an optional cumulative total for the quarter.
Useful for:
- Visualizing quarterly reports
- Aggregating financial or performance data
- Preparing charting datasets or grouped UI views
Steps:
groupDataByQuarter(data)
– Organizes data into a dictionary using keys like"2023-Q1"
based on thedate
field.initializeDates(groupedQuarters)
– Prepares a simplified structure listing quarters and associated month strings.initializeRestData(groupedQuarters)
– Flattens the quarterly structure into tax-specific groups, returning an array of metrics (profit
,profit_percent
) with their per-quarter values and totals.
This modular approach is helpful when formatting data for analytics dashboards or quarterly reports.
import { getQuarter, parseISO } from "date-fns";
// data
export const TaxEntitiesKeys = {
profit: "profit",
profit_percent: "profit_percent",
};
const mockTaxes = {
cumulative: [
{ date: "2023-01-31", profit: 1000, profit_percent: 100 },
{ date: "2023-02-28", profit: 2000, profit_percent: 200 },
{ date: "2023-04-30", profit: 3000, profit_percent: 250 },
],
report: [
{ date: "2023-01-31", profit: 300, profit_percent: 30 },
{ date: "2023-02-28", profit: 500, profit_percent: 50 },
{ date: "2023-04-30", profit: 700, profit_percent: 70 },
],
};
// data
// group by quarters
const groupDataByQuarter = (data) => {
const quarters = {};
// Group data into quarters based on their date
for (const monthData of data.report) {
const date = parseISO(monthData.date);
const year = date.getFullYear();
const quarter = getQuarter(date);
const key = `${year}-Q${quarter}`;
if (!quarters[key]) {
quarters[key] = { months: [], total: null };
}
quarters[key].months.push(monthData);
}
// Assign cumulative totals to their respective quarters
for (const totalData of data.cumulative) {
const date = parseISO(totalData.date);
const year = date.getFullYear();
const quarter = getQuarter(date);
const key = `${year}-Q${quarter}`;
if (quarters[key]) {
quarters[key].total = totalData;
}
}
return quarters;
};
const groupedQuarters = groupDataByQuarter(mockTaxes);
console.log(JSON.stringify({ groupedQuarters }, null, 2));
// group by quarters
// {
// groupedQuarters: {
// "2023-Q1": {
// months: [
// {
// date: "2023-01-31",
// profit: 300,
// profit_percent: 30,
// },
// {
// date: "2023-02-28",
// profit: 500,
// profit_percent: 50,
// },
// ],
// total: {
// date: "2023-02-28",
// profit: 2000,
// profit_percent: 200,
// },
// },
// "2023-Q2": {
// months: [
// {
// date: "2023-04-30",
// profit: 700,
// profit_percent: 70,
// },
// ],
// total: {
// date: "2023-04-30",
// profit: 3000,
// profit_percent: 250,
// },
// },
// },
// };
// dates
const initializeDates = (quarters) => {
const dates = [];
Object.entries(quarters).forEach(([dateKey, data]) => {
const [year, quarter] = dateKey.split("-Q").map(Number);
let yearEntry = dates.find((entry) => entry.year === year);
if (!yearEntry) {
yearEntry = { year, periods: [] };
dates.push(yearEntry);
}
yearEntry.periods.push({
months: data.months.map((month) => month.date),
quarter: `${quarter}`,
});
});
return dates;
};
console.log(
JSON.stringify({ dates: initializeDates(groupedQuarters) }, null, 2),
);
// dates
// {
// dates: [
// {
// year: 2023,
// periods: [
// {
// months: ["2023-01-31", "2023-02-28"],
// quarter: "1",
// },
// {
// months: ["2023-04-30"],
// quarter: "2",
// },
// ],
// },
// ],
// };
// rest data
const initializeRestData = (quarters) => {
return Object.keys(TaxEntitiesKeys).map((taxKey) => {
const taxKeyTyped = taxKey;
return {
title: taxKeyTyped,
periods: Object.entries(quarters).map(([_, data]) => ({
months: data.months.map((month) => month[taxKeyTyped]),
total: data.total ? data.total[taxKeyTyped] : null,
})),
};
});
};
console.log(
JSON.stringify({ restData: initializeRestData(groupedQuarters) }, null, 2),
);
// rest data
// {
// restData: [
// {
// title: "profit",
// periods: [
// {
// months: [300, 500],
// total: 2000,
// },
// {
// months: [700],
// total: 3000,
// },
// ],
// },
// {
// title: "profit_percent",
// periods: [
// {
// months: [30, 50],
// total: 200,
// },
// {
// months: [70],
// total: 250,
// },
// ],
// },
// ],
// };
innerJoin
Implements a basic innerJoin
function for
arrays — similar to SQL inner joins. This method takes a predicate
, a list of records
, and a list of ids
.
It returns all records where the predicate returns true for at least one ID.
This approach is helpful for:
- Matching entities across two datasets
- Resolving references between relational data
- Filtering records based on foreign key relations
The example joins musician records by matching their id
with an array of selected IDs.
function innerJoin(predicate, records, ids) {
return records.filter((record) => ids.some((id) => predicate(record, id)));
}
const result = innerJoin(
(record, id) => record.id === id,
[
{ id: 824, name: "Richie Furay" },
{ id: 956, name: "Dewey Martin" },
{ id: 313, name: "Bruce Palmer" },
{ id: 456, name: "Stephen Stills" },
{ id: 177, name: "Neil Young" },
],
[177, 456, 999],
);
console.log(result);
// [{id: 456, name: 'Stephen Stills'}, {id: 177, name: 'Neil Young'}]
list to tree
This utility transforms a flat list of items (with id
and parentId
fields) into a nested tree structure.
It uses a Map
to efficiently link children to their parents while preserving the hierarchy.
It also includes a treeToList()
function that flattens a tree back into a list — useful for data normalization,
storing tree nodes in databases, or syncing front-end trees with backend updates.
function listToTree(items) {
const map = new Map();
const roots = [];
// Map items by ID
for (const item of items) {
map.set(item.id, { ...item, children: [] });
}
for (const item of items) {
const node = map.get(item.id);
if (item.parentId === null) {
roots.push(node);
} else {
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(node);
}
}
}
return roots;
}
const items = [
{ id: 1, name: "Root 1", parentId: null },
{ id: 2, name: "Child 1.1", parentId: 1 },
{ id: 3, name: "Child 1.2", parentId: 1 },
{ id: 4, name: "Root 2", parentId: null },
{ id: 5, name: "Child 2.1", parentId: 4 },
{ id: 6, name: "SubChild 2.1.1", parentId: 5 },
];
const tree = listToTree(items);
console.log(tree);
// [
// {
// "id": 1,
// "name": "Root 1",
// "parentId": null,
// "children": [
// {
// "id": 2,
// "name": "Child 1.1",
// "parentId": 1,
// "children": []
// },
// {
// "id": 3,
// "name": "Child 1.2",
// "parentId": 1,
// "children": []
// }
// ]
// },
// {
// "id": 4,
// "name": "Root 2",
// "parentId": null,
// "children": [
// {
// "id": 5,
// "name": "Child 2.1",
// "parentId": 4,
// "children": [
// {
// "id": 6,
// "name": "SubChild 2.1.1",
// "parentId": 5,
// "children": []
// }
// ]
// }
// ]
// }
// ]
function treeToList(tree) {
const list = [];
function traverse(node) {
const { children, ...rest } = node;
list.push(rest);
if (children) {
for (const child of children) {
traverse(child);
}
}
}
for (const root of tree) {
traverse(root);
}
return list;
}
const flatList = treeToList(tree);
console.log(flatList);
// [
// { id: 1, name: 'Root 1', parentId: null },
// { id: 2, name: 'Child 1.1', parentId: 1 },
// { id: 3, name: 'Child 1.2', parentId: 1 },
// { id: 4, name: 'Root 2', parentId: null },
// { id: 5, name: 'Child 2.1', parentId: 4 },
// { id: 6, name: 'SubChild 2.1.1', parentId: 5 }
// ]
promises, closures and event loop mix
This section demonstrates how JavaScript promises, closures, and the event loop interact in real-world scenarios. These examples cover:
- How closures preserve state across async calls
- What happens when you
await
vs. run multiple async calls in parallel - The importance of capturing variables in closures before
setTimeout
- How the event loop schedules tasks with
setTimeout
,Promise
, andconsole.log
- Sequential chaining of promises with closures
Perfect for understanding async behavior, state retention, and timing execution in complex JavaScript code.
Example 1: Async function with closure and delayed execution
This example shows how closures capture state (count
) and how await
affects the execution order.
Each call to asyncCounter()
increments count
and logs it immediately. However, await
delays further code inside the async function,
allowing subsequent calls to update count
before the first finishes.
- Multiple calls to the same async function share the same closure state.
console.log("Script complete")
runs immediately becauseawait
yields control to the event loop.
function createAsyncCounter() {
let count = 0;
return async function incrementAsyncCounter() {
count++;
console.log({ count });
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Async Counter:", count);
};
}
const asyncCounter = createAsyncCounter();
asyncCounter();
asyncCounter().then(() => {
asyncCounter();
});
console.log("Script complete");
// { count: 1 }
// { count: 2 }
// Script complete
// 1 s delay
// Async Counter: 2
// Async Counter: 2
// { count: 3 }
// 1 s delay
// Async Counter: 3
Example 2: Sequential async calls with await
and closure
This example demonstrates how sequential await
calls work with closures. Each async call completes before the next one begins,
ensuring the counter increments correctly. The count
variable is preserved across invocations due to the closure, and each setTimeout
resolves after 1 second, leading to predictable, step-by-step output.
function createAsyncCounter() {
let count = 0;
return async function incrementAsyncCounter() {
count++;
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("Async Counter:", count);
};
}
const asyncCounter = createAsyncCounter();
await asyncCounter();
await asyncCounter();
await asyncCounter();
console.log("Script complete");
// with 1s between:
// Async Counter: 1
// Async Counter: 2
// Async Counter: 3
// immediately after counter 3:
// Script complete
Example 3: Task scheduling with closures and async timing
This example highlights how closure captures the shared taskCount
variable, which is incremented before each scheduled task.
All tasks reference the same taskCount
value by the time their setTimeout
callbacks run, resulting in each log displaying Task Count: 3
.
It also demonstrates how microtasks (like Promise.resolve().then
) run before timers and the event loop order between synchronous code, promises, and setTimeout
.
function createTaskScheduler() {
let taskCount = 0;
return function scheduleTask() {
taskCount++;
setTimeout(() => {
console.log("Task Count:", taskCount);
}, taskCount * 1000);
};
}
const scheduleTask = createTaskScheduler();
scheduleTask();
scheduleTask();
Promise.resolve().then(() => {
console.log("promise");
scheduleTask();
});
console.log("Tasks scheduled");
// Tasks scheduled
// promise
// after 1s with 1s between
// Task Count: 3
// Task Count: 3
// Task Count: 3
Example 4: Scheduling tasks with preserved state using closures
In this version, the closure captures the taskCount
value in a separate variable savedCount
before scheduling the setTimeout
.
This ensures that each task logs the correct value at the time it was created, rather than referencing the final shared taskCount
.
The result is that each setTimeout
logs a different value in increasing order, showcasing how to avoid timing issues by saving state early.
function createTaskScheduler() {
let taskCount = 0;
return function scheduleTask() {
taskCount++;
const savedCount = taskCount;
setTimeout(() => {
console.log("Task Count:", savedCount);
}, taskCount * 1000);
};
}
const scheduleTask = createTaskScheduler();
scheduleTask();
scheduleTask();
Promise.resolve().then(() => {
scheduleTask();
});
console.log("Tasks scheduled");
// Tasks scheduled
// after 1s with 1s between:
// Task Count: 1
// Task Count: 2
// Task Count: 3
Example 5: Preserving async state with closure in chained promises
This example demonstrates how closures can maintain and update state (count
) across multiple asynchronous calls.
Because the promise is chained (then -> then
), each counter()
call executes sequentially, allowing count
to increment between calls.
The use of setTimeout
inside the promise simulates asynchronous work, and the closure ensures the count
value is correctly preserved across executions.
function createCounter() {
let count = 0;
return function incrementCounter() {
count++;
return new Promise((resolve) => {
setTimeout(() => {
resolve(count);
}, 1000);
});
};
}
const counter = createCounter();
// if it was called at the same time, count would
// have not been saved in closure
counter()
.then((result) => {
console.log("Counter 1:", result);
return counter();
})
.then((result) => {
console.log("Counter 2:", result);
});
console.log("Script in progress");
// Script in progress
// after 1s with 1s between:
// Counter 1: 1
// Counter 2: 2
Example 6: Asynchronous multiplier using closure and chained promises
This example showcases how closures can maintain internal state (factor = 2
) in an asynchronous function.
The multiplyByTwo
function, returned by createAsyncMultiplier
, remembers the factor
across multiple chained .then()
calls.
Each multiplication is delayed with setTimeout
, and the result of one multiplication is passed to the next, demonstrating sequential asynchronous logic with shared state.
function createAsyncMultiplier() {
let factor = 2;
return function multiply(value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value * factor);
}, 1000);
});
};
}
const multiplyByTwo = createAsyncMultiplier();
multiplyByTwo(5)
.then((result) => {
console.log("Multiply 1:", result);
return multiplyByTwo(result);
})
.then((result) => {
console.log("Multiply 2:", result);
return multiplyByTwo(result);
})
.then((result) => {
console.log("Multiply 3:", result);
});
console.log("Multiplication started");
// Multiplication started
// after 1s with 1s between:
// Multiply 1: 10
// Multiply 2: 20
// Multiply 3: 40
Reducer pattern with actions
This example demonstrates the use of a reducer function similar to the pattern used in React’s useReducer
.
The reducer function handles different types of actions (added
, changed
, deleted
) to manage a list of tasks.
It accumulates state changes over time as actions are applied via
Array.prototype.reduce
,
making it a powerful pattern for predictable state updates in complex logic flows.
function tasksReducer(tasks, action) {
switch (action.type) {
case "added": {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
}
case "changed": {
return tasks.map((t) => {
if (t.id === action.id) {
const { type, ...actionNoType } = action;
return actionNoType;
} else {
return t;
}
});
}
case "deleted": {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error("Unknown action: " + action.type);
}
}
}
const initialState = [];
const actions = [
{ type: "added", id: 1, text: "Visit Kafka Museum" },
{ type: "added", id: 2, text: "Watch a puppet show" },
{ type: "deleted", id: 1 },
{ type: "added", id: 3, text: "Lennon Wall pic" },
{ type: "changed", id: 3, text: "Lennon Wall", done: true },
];
const finalState = actions.reduce(tasksReducer, initialState);
console.log(finalState);
// [
// { id: 2, text: 'Watch a puppet show', done: false },
// { id: 3, text: 'Lennon Wall', done: true }
// ]
Retry with exponential backoff
This snippet demonstrates how to implement a retry mechanism when fetching data from an API.
It attempts to fetch the resource multiple times (up to a given retries
limit) and waits a specified delay
between each retry.
If the request ultimately fails, it throws an error. This is particularly useful for handling flaky network requests
or ensuring robustness when dealing with unstable APIs.
async function fetchWithRetry(url, retries, delay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt === retries) {
throw new Error(`Failed to fetch after ${retries} retries`);
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
const res = await fetchWithRetry("https://pokeapi.co/api/v2/pokemon-color", 3);
console.log(res);
Shuffle (Fisher-Yates Algorithm)
This example implements the Fisher-Yates shuffle, a reliable way to randomly shuffle elements in an array. The algorithm works by iterating the array from the end to the beginning, swapping the current element with a randomly selected one from earlier in the array (or itself). It ensures a uniform distribution of permutations.
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
const shuffledArray = [1, 2, 3, 4, 5];
shuffle(shuffledArray);
console.log(shuffledArray); // [ 2, 1, 4, 5, 3 ]
Topological Sort
This example performs a topological sort on a set of items with dependencies. Each item can only appear in the result after all of its dependencies are resolved. It’s a common algorithm used in build systems, task schedulers, and dependency resolution tools.
const cards = [
{ id: 1, dependent: [6, 7, 8] },
{ id: 2, dependent: [6] },
{ id: 3, dependent: [] },
{ id: 4, dependent: [6, 7, 8] },
{ id: 5, dependent: [6, 8] },
{ id: 6, dependent: [] },
{ id: 7, dependent: [6] },
{ id: 8, dependent: [7] },
{ id: 9, dependent: [1] },
{ id: 10, dependent: [9] },
];
const getOrderedCards = (cards) => {
const result = [];
const added = new Set();
while (result.length < cards.length) {
for (const card of cards) {
if (
!added.has(card.id) &&
card.dependent.every((dep) => added.has(dep))
) {
result.push(card.id);
added.add(card.id);
}
}
}
return result;
};
console.log(getOrderedCards(cards));
// [
// 3, 6, 7, 8, 1,
// 2, 4, 5, 9, 10
// ]