Skip to main content

Command Palette

Search for a command to run...

JavaScript Decoded

Updated
23 min read
JavaScript Decoded

JavaScript Modules: Import and Export Explained

Why Modules Exist: The Problem First

Imagine building a house and putting every single thing — furniture, plumbing, wiring, walls — all in one giant room. That's what JavaScript looked like before modules.

In the early days, developers wrote everything in one massive .js file. As projects grew, this caused real problems:

  • Name collisions: Two different parts of your code accidentally use the same variable name, and one overwrites the other.

  • No clear ownership: You can't tell which function belongs to which feature.

  • Difficult maintenance: Changing one line can accidentally break something completely unrelated.

  • Zero reusability: You can't take a useful function and use it in another project without copying the whole file.

Consider this messy single-file scenario:

// app.js — everything crammed into one file 😓
var userName = "Alice";
var userAge = 25;

function greetUser() {
  console.log("Hello, " + userName);
}

var productName = "Laptop";
var productPrice = 999;

function displayProduct() {
  console.log(productName + " costs $" + productPrice);
}

function calculateTax(price) {
  return price * 0.18;
}
// ... 500 more lines below

Everything is tangled. Variables like userName and productName sit at the same level with no separation. This is where modules come in.


What Is a Module?

A module is simply a file that contains related code. Instead of one giant file, you split your code into focused, purposeful files — one for users, one for products, one for utilities.

Each module controls what it shares with the outside world using export, and requests what it needs from others using import.

Think of a module like a vending machine. It has stuff inside, but it only gives you what you press a button for. You don't get access to the internal mechanism — just the output.


Exporting: Sharing Code from a Module

To make something available to other files, you export it.

Named Exports

You can export multiple things from a single file by naming each export:

// mathUtils.js

export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;

You can also export everything at the bottom of the file:

// mathUtils.js

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

const PI = 3.14159;

export { add, subtract, PI };

Both approaches work identically. The second style is popular because it gives you a clear "public API" summary at the bottom.

Default Exports

A module can have one default export — typically when the file represents a single main concept:

// greet.js

export default function greetUser(name) {
  return `Hello, ${name}! Welcome.`;
}

Importing: Using Code from a Module

Once something is exported, another file can import it using the import keyword.

Importing Named Exports

You use curly braces to import named exports, and the name must match exactly:

// main.js

import { add, subtract, PI } from './mathUtils.js';

console.log(add(5, 3));       // 8
console.log(subtract(10, 4)); // 6
console.log(PI);              // 3.14159

Importing Default Exports

Default exports are imported without curly braces, and you can name them whatever you want:

// main.js

import greet from './greet.js';
// OR rename it:
import sayHello from './greet.js';

console.log(greet("Alice"));    // Hello, Alice! Welcome.
console.log(sayHello("Bob"));   // Hello, Bob! Welcome.

Importing Everything with * as

If you want all named exports under one object:

import * as MathUtils from './mathUtils.js';

console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.PI);        // 3.14159

Default vs Named Exports: When to Use Which

Feature Named Export Default Export
How many per file Multiple allowed Only one per file
Import syntax import { name } import anyName
Renaming on import import { name as alias } Just write any name
Best for Utilities, constants, helpers Main class/function of a file

Rule of thumb: Use default export when a file has one main purpose (like a UserCard component or a fetchData function). Use named exports when a file is a collection of related utilities.


Real-World Module Structure

Here's how a real project might be organized:

src/
├── utils/
│   ├── mathUtils.js      ← named exports
│   ├── stringUtils.js    ← named exports
├── services/
│   ├── apiService.js     ← default export
├── components/
│   ├── Header.js         ← default export
├── main.js               ← imports everything together
// stringUtils.js
export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function truncate(str, maxLength) {
  return str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
}
// main.js
import { capitalize, truncate } from './utils/stringUtils.js';
import apiService from './services/apiService.js';

console.log(capitalize("hello"));         // Hello
console.log(truncate("Long text here", 8)); // Long tex...

Benefits of Modular Code

Separation of concerns — Each file does one thing well.

Reusability — Export a utility once, import it anywhere across your project.

Maintainability — When a bug appears, you know exactly which module to look at.

Testability — Isolated modules are far easier to unit test.

Collaboration — Team members can work on different modules without stepping on each other.

Modules don't just organize code — they organize thinking. When you write modular code, you're forced to think about what each piece of your application actually does.


Template Literals in JavaScript

The Problem with Old-School String Concatenation

Before ES6 (2015), building dynamic strings in JavaScript was painful. You had to manually stitch together pieces using the + operator:

// The old way 😫
var firstName = "Alice";
var age = 25;
var city = "Mumbai";

var message = "Hello, my name is " + firstName + " and I am " + age + " years old. I live in " + city + ".";

console.log(message);
// Hello, my name is Alice and I am 25 years old. I live in Mumbai.

This approach has real problems:

  • Hard to read — You constantly switch between quotes and + signs.

  • Typo-prone — Missing a space before or after a + breaks your string silently.

  • Multi-line nightmares — Creating multi-line strings required \n escape characters or + continuation on every line.

  • No expression support — You can't put logic directly inside a string.

// Multi-line string — the old painful way 😖
var html = "<div>\n" +
           "  <h1>" + title + "</h1>\n" +
           "  <p>" + description + "</p>\n" +
           "</div>";

Enter Template Literals

Template literals were introduced in ES6 and immediately became one of the most-loved JavaScript features. Instead of quotes (' or "), you use backticks (`).

// Template literal syntax
const message = `This is a template literal`;

That alone does nothing new. The power comes from two features: variable embedding and multi-line support.


Embedding Variables and Expressions: ${}

Inside a template literal, anything inside ${ } gets evaluated as JavaScript:

const firstName = "Alice";
const age = 25;
const city = "Mumbai";

// Clean, readable, human-like 😍
const message = `Hello, my name is \({firstName} and I am \){age} years old. I live in ${city}.`;

console.log(message);
// Hello, my name is Alice and I am 25 years old. I live in Mumbai.

The difference in readability is night and day. But ${} isn't limited to variables — it evaluates any JavaScript expression:

const price = 500;
const quantity = 3;

// Math expression inside a string
console.log(`Total: ₹${price * quantity}`);
// Total: ₹1500

// Ternary operator inside a string
const isLoggedIn = true;
console.log(`Status: ${isLoggedIn ? "Welcome back!" : "Please log in."}`);
// Status: Welcome back!

// Function call inside a string
const name = "  alice  ";
console.log(`Hello, ${name.trim().toUpperCase()}!`);
// Hello, ALICE!

Multi-line Strings: No More \n

Template literals respect line breaks naturally. Just press Enter inside the backticks:

// Old way — messy 😩
const oldHtml = "<ul>\n" +
                "  <li>Item One</li>\n" +
                "  <li>Item Two</li>\n" +
                "</ul>";

// New way — exactly as you'd write it ✅
const newHtml = `
<ul>
  <li>Item One</li>
  <li>Item Two</li>
</ul>
`;

console.log(newHtml);

This is especially valuable when building HTML strings, SQL queries, email templates, or multi-line messages in your code.


Side-by-Side Comparison

const user = { name: "Bob", role: "Admin", score: 95 };

// ❌ Old concatenation
var oldMsg = "User: " + user.name + "\nRole: " + user.role + "\nScore: " + user.score + "/100";

// ✅ Template literal
const newMsg = `
User:  ${user.name}
Role:  ${user.role}
Score: ${user.score}/100
`;

Tagged Template Literals (Advanced Use Case)

Template literals can also be tagged — you pass them through a function before the string is created:

function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? `[${values[i]}]` : '');
  }, '');
}

const item = "JavaScript";
const price = 999;

console.log(highlight`Learn \({item} for ₹\){price} only!`);
// Learn [JavaScript] for [999] only!

Tagged templates power libraries like styled-components in React and SQL query builders that need safe parameter escaping.


Modern Use Cases

Template literals shine in real-world scenarios:

1. Building API URLs

const baseURL = "https://api.example.com";
const userId = 42;
const endpoint = `\({baseURL}/users/\){userId}/profile`;

2. Generating HTML dynamically

const products = ["Laptop", "Phone", "Tablet"];

const listHTML = `
<ul>
  \({products.map(p => `<li>\){p}</li>`).join('\n  ')}
</ul>
`;

3. Console logging for debugging

const fn = "calculateTotal";
const val = 1500;
console.log(`[DEBUG] Function: \({fn} | Result: \){val}`);

Key Takeaways

Feature Old String Template Literal
Syntax '...' + var + '...' `...${var}...`
Multi-line Needs \n + + Natural line breaks
Expressions Not possible Full JS in ${}
Readability Low High
Tagged templates No Yes

Template literals don't just save keystrokes — they make your code read like English. When code reads naturally, it's easier to reason about, easier to debug, and easier for teammates to understand.


Callbacks in JavaScript: Why They Exist

Functions Are First-Class Citizens

Before understanding callbacks, you need to understand one fundamental thing about JavaScript: functions are values.

Just like a number or a string, a function can be:

  • Stored in a variable

  • Passed as an argument to another function

  • Returned from a function

// A function stored in a variable
const greet = function(name) {
  return `Hello, ${name}!`;
};

// Passing a function as an argument
function callTwice(fn) {
  fn();
  fn();
}

function sayHi() {
  console.log("Hi!");
}

callTwice(sayHi);
// Hi!
// Hi!

In callTwice(sayHi) — notice we pass sayHi without parentheses. We're passing the function itself, not calling it. This is the foundation of callbacks.


What Is a Callback Function?

A callback is a function that you pass to another function, to be called back (executed) later — either immediately or after some operation completes.

function doMath(a, b, operation) {
  return operation(a, b); // "calling back" the function we received
}

function add(x, y) { return x + y; }
function multiply(x, y) { return x * y; }

console.log(doMath(5, 3, add));       // 8
console.log(doMath(5, 3, multiply));  // 15

operation here is a callback. doMath doesn't know (or care) what the operation is — it just knows it will receive a function and call it.


Why Callbacks Are Essential: The Async Problem

JavaScript runs in a single thread. It can only do one thing at a time. But many operations take time:

  • Fetching data from a server

  • Reading a file from disk

  • Waiting for a timer to expire

  • Listening for a button click

If JavaScript had to wait (block) for each of these, your entire page would freeze. Try clicking a button on a webpage while a 5-second fetch is happening — without async, that would be impossible.

The solution? Don't wait. Come back when it's done.

// setTimeout is a built-in that calls your callback after a delay
console.log("Step 1: Order placed");

setTimeout(function() {
  console.log("Step 3: Order delivered (2 seconds later)");
}, 2000);

console.log("Step 2: Doing other things...");

// Output:
// Step 1: Order placed
// Step 2: Doing other things...
// Step 3: Order delivered (2 seconds later)

JavaScript didn't stop and wait. It registered the callback, moved on, and came back when the timer fired.


Callbacks in Common Scenarios

1. Array Methods

Built-in array methods like forEach, map, and filter all use callbacks:

const numbers = [1, 2, 3, 4, 5];

// forEach — callback runs once for each element
numbers.forEach(function(num) {
  console.log(num * 2);
});

// map — callback transforms each element, returns new array
const doubled = numbers.map(function(num) {
  return num * 2;
});
// [2, 4, 6, 8, 10]

// filter — callback tests each element, keeps those that pass
const evens = numbers.filter(function(num) {
  return num % 2 === 0;
});
// [2, 4]

2. Event Listeners

const button = document.getElementById("myButton");

// The second argument is a callback — called when button is clicked
button.addEventListener("click", function() {
  console.log("Button was clicked!");
});

The browser doesn't know when the user will click. So you hand it a callback and say: "When they click, run this."

3. Simulating Async Data Fetch

function getUserData(userId, callback) {
  // Simulating a network delay with setTimeout
  setTimeout(function() {
    const user = { id: userId, name: "Alice", email: "alice@example.com" };
    callback(user); // "Here's the data, now call back with it"
  }, 1500);
}

getUserData(101, function(user) {
  console.log(`Got user: ${user.name}`); // Got user: Alice
});

console.log("Fetching user data..."); // This runs first

The Problem: Callback Hell

Callbacks work great for one or two levels. But what if you need multiple async operations in sequence? Things get ugly fast:

// "Callback Hell" — also known as the "Pyramid of Doom" 😱
loginUser("alice", function(user) {
  getUserProfile(user.id, function(profile) {
    getUserPosts(profile.id, function(posts) {
      getPostComments(posts[0].id, function(comments) {
        // We're now 4 levels deep...
        console.log(comments);
      });
    });
  });
});

This code is:

  • Hard to read — indentation keeps growing rightward

  • Hard to maintain — error handling at each level is a mess

  • Hard to debug — stack traces are confusing

This is a real problem that JavaScript developers experienced for years. It's exactly why Promises and async/await were invented — but understanding callbacks is essential before you can appreciate those solutions.


Key Takeaways

Concept Description
Callback A function passed as an argument to be called later
Synchronous callback Called immediately (e.g., map, forEach)
Asynchronous callback Called after an operation completes (e.g., setTimeout, fetch)
Callback hell Deeply nested callbacks that hurt readability
Modern solution Promises and async/await solve callback hell

Callbacks are not a bug — they're the foundation of async JavaScript. Promises and async/await are just cleaner ways to write the same idea. Understanding callbacks makes everything else click.


The new Keyword in JavaScript

The Problem: Creating Many Similar Objects

Suppose you're building a user management system. You need to create objects for each user:

// Tedious manual approach 😓
const user1 = { name: "Alice", age: 25, role: "admin" };
const user2 = { name: "Bob",   age: 30, role: "user"  };
const user3 = { name: "Carol", age: 22, role: "user"  };

This works, but it doesn't scale. There's no blueprint, no shared behavior, and no connection between these objects. If you want to add a method to all users, you'd have to add it to each object manually.

JavaScript solves this with constructor functions and the new keyword.


Constructor Functions: The Blueprint

A constructor function is a regular function written with a capital first letter (by convention) that acts as a template for creating objects:

function User(name, age, role) {
  this.name = name;
  this.age  = age;
  this.role = role;
}

Notice this — when you call this function with new, this will refer to the brand new object being created.


What new Does, Step by Step

When you write new User("Alice", 25, "admin"), JavaScript does four things automatically:

Step 1 — Creates a new empty object {}

Step 2 — Links that new object's prototype to User.prototype

Step 3 — Executes the constructor function, with this pointing to the new object

Step 4 — Returns the new object automatically (unless you explicitly return a different object)

function User(name, age, role) {
  // Step 3: 'this' is the new empty object
  this.name = name;
  this.age  = age;
  this.role = role;
  // Step 4: new automatically returns 'this'
}

const alice = new User("Alice", 25, "admin");
const bob   = new User("Bob",   30, "user");

console.log(alice.name); // Alice
console.log(bob.age);    // 30
console.log(alice instanceof User); // true

alice and bob are separate instances — they share the same blueprint but hold their own data.


What Happens Without new (The Danger)

If you forget new, this no longer refers to a new object. In non-strict mode, it refers to the global object (window in browsers):

function User(name) {
  this.name = name;
}

const user = User("Alice"); // ⚠️ Missing 'new'!

console.log(user);        // undefined — nothing was returned
console.log(window.name); // "Alice" — oops! Polluted the global scope

This is why constructor function names are capitalized by convention — it's a visual reminder that you must use new with them.


Adding Methods via Prototype

You could add methods directly inside the constructor:

function User(name, age) {
  this.name = name;
  this.age  = age;
  this.greet = function() { // ⚠️ A new copy created for EVERY instance
    return `Hi, I'm ${this.name}`;
  };
}

But this creates a new function in memory for every single user object. With 1000 users, that's 1000 identical functions.

The better way is to add methods to User.prototype — they'll be shared across all instances:

function User(name, age) {
  this.name = name;
  this.age  = age;
}

// Added ONCE, shared by ALL instances ✅
User.prototype.greet = function() {
  return `Hi, I'm \({this.name} and I'm \){this.age} years old.`;
};

User.prototype.isAdult = function() {
  return this.age >= 18;
};

const alice = new User("Alice", 25);
const bob   = new User("Bob", 15);

console.log(alice.greet());   // Hi, I'm Alice and I'm 25 years old.
console.log(bob.isAdult());   // false

Every object created with new User() has a hidden link (__proto__) pointing to User.prototype. When JavaScript looks up a property and can't find it on the object itself, it walks up this prototype chain:

console.log(alice.greet);
// JavaScript checks: does alice have 'greet'? No.
// Checks alice.__proto__ (which is User.prototype). Yes! Found it.

console.log(alice.__proto__ === User.prototype); // true

This is JavaScript's inheritance mechanism — lean, efficient, and shared.


The Modern Alternative: ES6 Classes

ES6 introduced class syntax — it's not a new system, just cleaner syntax over the same prototype mechanism:

class User {
  constructor(name, age) {
    this.name = name;
    this.age  = age;
  }

  greet() {
    return `Hi, I'm ${this.name}`;
  }

  isAdult() {
    return this.age >= 18;
  }
}

const alice = new User("Alice", 25);
console.log(alice.greet()); // Hi, I'm Alice

Under the hood, class uses the exact same new + prototype mechanism. Understanding new means you understand what class is actually doing.


Summary

Step What Happens
new User() called Empty object {} is created
Prototype link object.__proto__ = User.prototype
Constructor runs this = new object, properties assigned
Return New object returned automatically
Prototype methods Shared across all instances (memory efficient)

The new keyword is JavaScript's factory. Every new call produces an independent instance sharing the same behavior blueprint. Once you grasp this, ES6 classes, inheritance, and instanceof all start making perfect sense.


String Polyfills and Common Interview Methods in JavaScript

What Are String Methods?

JavaScript strings come with a rich set of built-in methods — functions you call on any string value to transform, search, or analyze it:

const str = "  Hello, World!  ";

str.trim()           // "Hello, World!"      — removes whitespace
str.toUpperCase()    // "  HELLO, WORLD!  "  — converts to uppercase
str.includes("World") // true               — checks if substring exists
str.slice(2, 7)      // "Hello"             — extracts a portion
str.replace("World", "JavaScript") // "  Hello, JavaScript!  "

These are battle-tested, optimized, and available in every modern browser. But understanding how they work — and being able to write them yourself — is what separates junior developers from strong engineers.


What Is a Polyfill?

A polyfill is code you write yourself to replicate the behavior of a built-in feature — usually because that feature doesn't exist in older environments, or because you want to understand it deeply.

// The built-in already exists:
"hello".toUpperCase(); // "HELLO"

// A polyfill teaches you HOW it could work:
String.prototype.myToUpperCase = function() {
  let result = '';
  for (let i = 0; i < this.length; i++) {
    const code = this.charCodeAt(i);
    // Lowercase letters are ASCII 97-122
    if (code >= 97 && code <= 122) {
      result += String.fromCharCode(code - 32); // Shift to uppercase range
    } else {
      result += this[i];
    }
  }
  return result;
};

console.log("hello".myToUpperCase()); // "HELLO"

Writing polyfills forces you to think about edge cases, character encoding, and performance — exactly what interviewers want to see.


Polyfill 1: String.prototype.trim

trim() removes whitespace from both ends of a string.

String.prototype.myTrim = function() {
  let start = 0;
  let end = this.length - 1;

  // Move start pointer past leading spaces
  while (start <= end && this[start] === ' ') {
    start++;
  }

  // Move end pointer before trailing spaces
  while (end >= start && this[end] === ' ') {
    end--;
  }

  return this.slice(start, end + 1);
};

console.log("   hello world   ".myTrim()); // "hello world"
console.log("   ".myTrim());              // ""
console.log("no spaces".myTrim());        // "no spaces"

Polyfill 2: String.prototype.includes

includes(searchStr) returns true if the string contains the given substring.

String.prototype.myIncludes = function(searchStr) {
  // Edge case: empty string is always "included"
  if (searchStr === '') return true;
  if (searchStr.length > this.length) return false;

  for (let i = 0; i <= this.length - searchStr.length; i++) {
    if (this.slice(i, i + searchStr.length) === searchStr) {
      return true;
    }
  }
  return false;
};

console.log("Hello World".myIncludes("World")); // true
console.log("Hello World".myIncludes("xyz"));   // false
console.log("Hello World".myIncludes(""));      // true

Polyfill 3: String.prototype.repeat

repeat(n) returns the string repeated n times.

String.prototype.myRepeat = function(count) {
  if (count < 0) throw new RangeError("Invalid count value");
  if (count === 0) return '';

  let result = '';
  for (let i = 0; i < count; i++) {
    result += this;
  }
  return result;
};

console.log("ha".myRepeat(3)); // "hahaha"
console.log("ab".myRepeat(0)); // ""
console.log("-".myRepeat(5)); // "-----"

Polyfill 4: String.prototype.startsWith

String.prototype.myStartsWith = function(prefix, startPos = 0) {
  // Can't start with something longer than the string
  if (prefix.length > this.length - startPos) return false;

  for (let i = 0; i < prefix.length; i++) {
    if (this[startPos + i] !== prefix[i]) return false;
  }
  return true;
};

console.log("JavaScript".myStartsWith("Java"));     // true
console.log("JavaScript".myStartsWith("Script"));   // false
console.log("JavaScript".myStartsWith("Script", 4)); // true

Common Interview String Problems

These are questions that regularly appear in technical interviews:

Problem 1: Reverse a String

function reverseString(str) {
  let reversed = '';
  for (let i = str.length - 1; i >= 0; i--) {
    reversed += str[i];
  }
  return reversed;
}

console.log(reverseString("hello"));   // "olleh"
console.log(reverseString("racecar")); // "racecar"

Problem 2: Check If a String Is a Palindrome

function isPalindrome(str) {
  const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
  const reversed = cleaned.split('').reverse().join('');
  return cleaned === reversed;
}

console.log(isPalindrome("racecar"));    // true
console.log(isPalindrome("A man a plan a canal Panama")); // true
console.log(isPalindrome("hello"));      // false

Problem 3: Count Character Occurrences

function charCount(str) {
  const counts = {};
  for (let char of str) {
    counts[char] = (counts[char] || 0) + 1;
  }
  return counts;
}

console.log(charCount("banana"));
// { b: 1, a: 3, n: 2 }

Problem 4: Find the Most Frequent Character

function mostFrequentChar(str) {
  const counts = {};
  let maxChar = '';
  let maxCount = 0;

  for (let char of str) {
    counts[char] = (counts[char] || 0) + 1;
    if (counts[char] > maxCount) {
      maxCount = counts[char];
      maxChar = char;
    }
  }
  return maxChar;
}

console.log(mostFrequentChar("javascript")); // "a"

Problem 5: Implement String.prototype.indexOf from scratch

String.prototype.myIndexOf = function(searchStr, fromIndex = 0) {
  if (searchStr === '') return fromIndex;

  for (let i = fromIndex; i <= this.length - searchStr.length; i++) {
    if (this.slice(i, i + searchStr.length) === searchStr) {
      return i;
    }
  }
  return -1;
};

console.log("hello world".myIndexOf("world")); // 6
console.log("hello world".myIndexOf("xyz"));   // -1
console.log("hello world".myIndexOf("l"));     // 2

Why This Matters for Interviews

Interviewers asking you to implement string methods aren't being cruel — they're testing real skills:

What They Ask What They're Testing
Reverse a string Loop control, index manipulation
Implement trim Pointer technique, edge cases
Check palindrome String comparison logic
Count characters Hash map / object usage
Implement includes Sliding window thinking

The moment you know how includes works internally — scanning with a window of the same length as the search string — you'll never forget it. And you'll write better code because you understand what's happening under the hood.


Key Takeaways

  • String methods are built-in tools, but knowing their inner logic makes you a stronger programmer.

  • Polyfills are the best way to deeply understand how something works.

  • Interview questions about strings almost always test: loops, indexing, character comparison, and edge cases.

  • Always think about edge cases: empty strings, single characters, strings with only spaces.

The best programmers aren't those who memorize the most APIs. They're those who understand what those APIs are doing — and can reason from first principles when something breaks.