JavaScript Guide
The language of the web, end to end β variables, functions, arrays, objects, the DOM, events, promises, async/await, fetch, and modules. Every code block has a Copy button.
Copy button on each code block Β· β Beginner β Intermediate β Advanced01 Introduction β The Language of the Web
JavaScript (JS) is the only programming language that runs natively in every web browser. It adds behaviour to a page: what happens when you click, what appears after a request, how data is validated, how the UI updates. If HTML is the skeleton and CSS is the skin, JavaScript is the muscle and nervous system.
What makes JavaScript different
- Dynamic typing β variables have no fixed type;
let x = 1; x = 'text';is legal - Single-threaded + event loop β one call stack, but non-blocking I/O via callbacks, promises and the event loop
- Multi-paradigm β procedural, object-oriented and functional styles all work
- First-class functions β functions are values: pass them, return them, store them
- Prototype-based β objects inherit directly from other objects (classes in ES6 are syntax sugar over this)
Where JavaScript runs
A very short history
- 1995 β Brendan Eich creates JavaScript at Netscape in about 10 days
- 1997 β standardized as ECMAScript (ES) β JS is the language, ECMAScript is the spec
- 2009 β ES5: strict mode, JSON, Array methods like
map - 2015 β ES6/ES2015: the big one β
let/const, arrow functions, classes, promises, modules, template literals - 2016 β now β one feature release every year (ES2016, ES2017 β¦ ES2026): async/await, optional chaining
?., nullish coalescing??and more
02 Setup & Dev Tools Beginner
Three ways to run JavaScript
<!-- 1. External file (recommended β always) -->
<script src="app.js" defer></script>
<!-- 2. Inline block (small sites, demos only) -->
<script>
console.log('Hello from the page');
</script>
<!-- 3. Inline attribute (avoid β mixing behaviour into HTML) -->
<button onclick="alert('hi')">Click</button>
// app.js
const button = document.querySelector('#save');
button.addEventListener('click', () => {
console.log('Saved!');
});
defer vs async
<head> without defer runs before the HTML below it exists β querySelector returns null and you get "Cannot read properties of null". Always add defer (or put the script at the end of <body>).Console methods you'll actually use
console.log('plain', user); // multiple values, one group
console.warn('deprecated'); // yellow warning
console.error('failed'); // red error + stack trace
console.table(users); // rendered table for arrays/objects
console.dir(user); // live object, expandable
console.group('Step 1'); // nested groups
console.groupEnd();
console.time('load'); β¦ console.timeEnd('load'); // timing
console.assert(user, 'user is required'); // logs only when falsy
console.trace(); // where was this called from?
Browser DevTools β your superpower
Node.js β JavaScript outside the browser
node -v # check installed version
node app.js # run a file
node # REPL β evaluate JS interactively
npm init -y # create package.json
npm install express # add a package (creates node_modules)
npm install -D nodemon # dev-only dependency
fs, http, process β and has no document or window. Browser code has the DOM β and no file system.Create index.html + app.js (with defer), log document.title in the console, then set a breakpoint on that line and reload. Inspect what's in scope.
03 Variables & Data Types Beginner
let, const, var
const siteName = 'Laksh Guides'; // block-scoped, cannot be reassigned β DEFAULT
let theme = 'dark'; // block-scoped, will be reassigned
var old = 'legacy'; // function-scoped, hoisted, ambiguous β avoid
theme = 'light'; // OK β let allows reassignment
// siteName = 'Other'; // TypeError: assignment to constant
- Prefer
constβ signal "this never changes". Switch toletonly when you must reassign constis not deeply immutable β you cannot reassign the variable, but you can mutate the object/array it points tovarproblems β function-scoped instead of block-scoped, hoisted to the top (usable before declaration asundefined), no TDZ errors. Only needed for old codebases
const user = { name: 'Laksh' };
user.name = 'Ron'; // allowed β mutating the object
// user = {}; // NOT allowed β reassigning the constant
let and const exist only inside the nearest { } β loops, ifs, functions. This prevents the classic "loop variable leaked everywhere" bug that var had.Primitive types (7)
typeof 'hi' // "string"
typeof 42 // "number"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" β historical bug, always was
typeof [] // "object" β arrays are objects
typeof () => {} // "function"
Array.isArray([]) // true β the correct array check
typeof NaN // "number" β NaN = Not a Number, still a number type
Dynamic typing & coercion
let x = 1; // number
x = 'one'; // now a string β no error
// == coerces types before comparing (avoid it)
5 == '5' // true
0 == '' // true β surprise!
// === compares type AND value (always use this)
5 === '5' // false
5 === 5 // true
=== and !==. The only time == is safe is x == null, which checks null or undefined in one shot β everything else should be strict.Converting values
// To string
String(42) // "42"
(42).toString() // "42"
`42` // "42"
// To number
Number('42') // 42
'42' * 1 // 42 β trick used in old code
parseInt('42px', 10) // 42 β stop at first non-digit (pass radix 10)
parseFloat('3.5m') // 3.5
Number('abc') // NaN
// To boolean β only 8 falsy values exist
Boolean(0) // false
Boolean('') // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean('0') // true β non-empty string!
0.1 + 0.2 === 0.3 is false (0.30000000000000004) β all languages with binary floating point share this. For money, store paise/cents as integers (βΉ1 = 100) or use a decimal library. Compare floats with Math.abs(a - b) < 0.0001, never with ===.04 Operators & Control Flow Beginner
Operators at a glance
// Arithmetic
+ - * / % **
% = modulo (remainder) β "is even?" β n % 2 === 0
** = exponent β 2 ** 10 === 1024
+= -= *= /= β count += 1
++ -- β count++ (avoid inside expressions)
// Comparison
=== !== // strict β ALWAYS use these
< > <= >=
// Logical
&& both must be true β isAdult && hasId
|| at least one true β isAdmin || isOwner
! negation β !isLoading
?? nullish coalescing β nickname ?? name // only null/undefined
?. optional chaining β user?.address?.city
Truthiness β the 8 falsy values
Everything in JavaScript is truthy except these eight:
'0', 'false' and [] (empty array) are truthy β they are non-empty values. So if (value) on the string '0' passes. To check "has content", be explicit: value.length > 0.Conditionals
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
// Ternary β one-line if/else (readable only when short)
const label = isLoggedIn ? 'Dashboard' : 'Login';
// switch β remember break (or it "falls through")
switch (day) {
case 'Sat':
case 'Sun':
isWeekend = true;
break;
case 'Mon':
isWeekend = false;
break;
default:
isWeekend = false;
}
switch or a lookup object is cleaner than nested else if: const icons = { sun:'βοΈ', moon:'π' }; icons[day] ?? 'β';Loops
// Classic for β when you need the index or custom stepping
for (let i = 0; i < items.length; i++) { console.log(i, items[i]); }
// while β repeats until condition turns false
while (attempts < 3) { attempts++; }
// for...of β VALUES of any iterable (arrays, strings, maps) β PREFERRED
for (const item of items) { console.log(item); }
for (const [i, item] of items.entries()) { console.log(i, item); }
// for...in β KEYS of an object (works on arrays too β don't use it there)
for (const key in user) { console.log(key, user[key]); }
// Control
for (const n of numbers) {
if (n % 2 !== 0) continue; // skip odds
if (n > 100) break; // stop entirely
}
for...in on arrays. It iterates string keys ('0', '1') and can pick up inherited properties. Use for...of, forEach or a classic for loop instead.Print a multiplication table (1β10) for a number using a nested loop, then rewrite it with Array.from and map when you reach the Arrays section. Notice how the data-flow thinking changes.
05 Functions, Scope & Closures Intermediate
A function is a reusable block of behaviour. In JavaScript, functions are values β you can store them in variables, pass them to other functions and return them from functions.
Four ways to write one
// 1. Declaration β hoisted (usable before its line)
function add(a, b) { return a + b; }
// 2. Function expression β created at runtime
const mul = function (a, b) { return a * b; };
// 3. Arrow function β concise, lexical `this` (ideal for callbacks)
const sub = (a, b) => a - b;
const square = n => n * n; // single param: no parens
const noop = () => {}; // no params: empty parens required
// 4. Async function β always returns a promise
async function loadUser(id) { return fetchUser(id); }
n => n * n. To return an object literal, wrap it: () => ({ name: 'Laksh' }). Missing braces here is the #1 arrow-function bug.Parameters β defaults and rest
function greet(name = 'friend', greeting = 'Hi') {
return `${greeting}, ${name}!`;
}
greet() // "Hi, friend!"
greet('Laksh') // "Hi, Laksh!"
// Rest β collects remaining arguments into a real array
function max(...nums) {
return nums.reduce((a, b) => (b > a ? b : a), -Infinity);
}
max(3, 9, 4) // 9
Scope & closures
Scope = where a variable is visible. Inner scopes can read outer variables; outer scopes cannot see inside. A closure is a function that remembers the variables from the place it was created β even after that place has finished running.
function createCounter() {
let count = 0; // private β hidden from outside
return {
inc: () => ++count,
get: () => count,
reset: () => { count = 0; }
};
}
const counter = createCounter();
counter.inc(); // 1
counter.inc(); // 2
counter.get(); // 2
// count is NOT accessible here β the closure protects it
- Uses of closures: private state (counters, stores), function factories, memoization/caching, event handlers with captured data
- Scope chain: global β function β block. Reading is fine up the chain; writing to an outer variable from inside is where bugs start β pass values in instead
- Hoisting: declarations (
function f(){},var) move to the top of their scope;let/constand expressions do not β they throwReferenceErrorin the temporal dead zone (TDZ)
Functions as values (callbacks)
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2); // [2,4,6,8] β a function passed in
button.addEventListener('click', () => { // function given to another
console.log('clicked');
}); // block of code, called later by JS
// Function factory via closure
function multiplyBy(factor) {
return n => n * factor;
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5) // 10
this in 30 seconds: regular functions get this from how they are called (obj.fn() β obj). Arrow functions have no this of their own β they inherit it from the surrounding function. That's why arrows are perfect for callbacks inside methods.06 Arrays & Array Methods Intermediate
const fruits = ['apple', 'banana', 'mango'];
const nums = [10, 20, 30];
const mixed = [1, 'two', true, { id: 1 }, []]; // JS arrays can hold anything
fruits.length // 3
fruits[0] // "apple" β indexes start at 0
fruits.at(-1) // "mango" β negative index from the end
fruits[fruits.length - 1] // "mango" β old way
Transforming β map, filter, reduce
const users = [
{ name: 'Laksh', age: 20, active: true },
{ name: 'Asha', age: 23, active: false },
{ name: 'Rahul', age: 19, active: true },
];
users.map(u => u.name) // ["Laksh","Asha","Rahul"] β transform, same length
users.filter(u => u.active) // 2 users β keep matches
users.reduce((sum, u) => sum + u.age, 0) // 62 β fold into one value
users.find(u => u.age < 21) // {Lakshβ¦} β first match or undefined
users.findIndex(u => u.name === 'Asha') // 1
users.some(u => !u.active) // true β at least one
users.every(u => u.age >= 18) // true β all of them
map changes every item, filter removes items, reduce squashes everything into a single value (sum, object, count). Master these three and 90% of array problems shrink to one line.Adding, removing, rearranging
// β sort() without a comparator sorts LEXICOGRAPHICALLY (as text)
[10, 9, 100].sort() // [10, 100, 9] β wrong
[10, 9, 100].sort((a, b) => a - b) // [9, 10, 100] β numeric
// Sorting objects
users.sort((a, b) => a.age - b.age); // ascending
users.sort((a, b) => a.name.localeCompare(b.name)); // alphabetical
Iteration styles
items.forEach((item, i) => console.log(i, item)); // no return value
for (const item of items) { β¦ } // supports break/continue
items
.filter(u => u.active)
.map(u => u.name)
.sort(); // chaining reads like a pipeline
push, pop, shift, unshift, splice, sort, reverse, fill change the original array β dangerous when that array is state shared with a UI (React/Vue). For immutable code, copy first: [...arr].sort().forEach cannot break out early β use a plain for...of, or the trick arr.some(x => { β¦; return true; }) for an early exit.07 Objects, Spread & Destructuring Intermediate
Objects are collections of key: value pairs β the main data structure for everything shaped (a user, a product, a config, API responses).
const user = {
name: 'Laksh', // shorthand: same as name: name
age: 20,
email: 'laksh@example.com',
active: true,
tags: ['dev', 'student'],
address: { city: 'Mangalore', pin: 575001 },
// method β `this` refers to this object
greet() { return `Hi, I'm ${this.name}`; },
['computed_' + 'key']: true, // computed key
};
user.name // dot access (keys must be valid identifiers)
user['email'] // bracket access β also for dynamic keys
const field = 'age';
user[field] // 20
user.city = 'Udupi'; // add
delete user.active; // remove
'name' in user // true β key exists check
Object.hasOwn(user, 'name') // true β safer (ignores prototype chain)
Iterating over objects
Object.keys(user) // ["name","age",β¦] β array of keys
Object.values(user) // ["Laksh",20,β¦] β array of values
Object.entries(user) // [["name","Laksh"],β¦] β [key, value] pairs
for (const [key, value] of Object.entries(user)) {
console.log(key, 'β', value);
}
// Or directly
for (const key in user) { console.log(key, user[key]); }
Destructuring β unpack values
const { name, age, role = 'student' } = user; // default if undefined
const { name: fullName } = user; // rename
const { address: { city } } = user; // nested
const { password, ...safeUser } = user; // rest β omit one
// In function parameters (very common in React)
function renderCard({ title, subtitle = '', pinned = false }) { β¦ }
renderCard({ title: 'Hello' });
Spread & copying
const copy = { ...user }; // shallow copy
const updated = { ...user, age: 21 }; // copy + change (immutable update)
const merged = { ...defaults, ...user }; // later keys win
const a = { x: 1 };
const b = a; // β SAME reference β not a copy!
b.x = 2; // a.x is also 2 now
const c = { ...a }; // β
real (shallow) copy
// Deep copy (nested objects) β modern way
const deep = structuredClone(user); // handles nested, dates, maps
const legacy = JSON.parse(JSON.stringify(user)); // functions/dates lost
{a:1} === {a:1} is false. This breaks includes on arrays of objects β match by a unique id instead: list.find(x => x.id === id).JSON.stringify drops functions and undefined, converts Date to a string, and throws on circular references. For saving to localStorage or sending to an API that's exactly what you want β just remember it's a one-way trip for special types.08 The DOM β Making the Page Interactive Intermediate
The DOM (Document Object Model) is the tree of objects the browser builds from your HTML. Every tag becomes an element node, every piece of text a text node. JavaScript reads and edits this tree, and the browser re-renders the page.
window vs document: document is the page's HTML tree; window is the whole browser context (scrollbar, timers, localStorage, global variables). window.document β the same document.Selecting elements
// Returns ONE element or null
document.querySelector('#save'); // any CSS selector
document.querySelector('.card.title');
document.querySelector('input[name="email"]');
// Returns a LIVE HTMLCollection (use [i] or [...list])
document.getElementsByClassName('btn');
document.getElementsByName('q');
// Returns a STATIC NodeList
document.querySelectorAll('.btn'); // forEach works directly β
// Direct references
document.getElementById('header');
document.title; document.body; document.documentElement; // <html>
querySelector accepts any CSS selector you already know. Give elements id for singletons and class/data-* attributes for groups β e.g. [data-theme="dark"], [hidden].Changing content & attributes
el.textContent = 'Hello'; // safe β escapes HTML
el.innerHTML = '<b>Hello</b>'; // β parses HTML β NEVER with user input (XSS)
el.value; // for <input> / <textarea> β read
el.value = 'typed text'; // set
el.checked = true; // checkboxes / radios
el.disabled = true;
el.hidden = true; // boolean attribute
el.setAttribute('href', '/docs');
el.getAttribute('href');
el.removeAttribute('disabled');
el.dataset.id // data-id β dataset.id (camelCase: data-user-id β dataset.userId)
el.classList.add('active');
el.classList.remove('active');
el.classList.toggle('active');
el.classList.contains('active'); // true/false
el.style.color = 'crimson'; // inline styles β prefer CSS classes
textContent, never innerHTML. innerHTML with attacker data executes their scripts. Use textContent for text; if you must inject markup, sanitize it first.Creating, inserting, removing nodes
// Create
const li = document.createElement('li');
li.textContent = 'Task 3';
li.classList.add('todo');
li.dataset.id = '3';
// Insert
list.append(li); // end of list (multiple args ok)
list.prepend(li); // start
list.append(document.createElement('hr'));
li.after(el); li.before(el); // siblings
list.appendChild(li); // old API β one node only
list.insertBefore(li, list.firstChild);
// Insert HTML string (trusted content only)
list.insertAdjacentHTML('beforeend', '<li>Hi</li>');
// Remove
li.remove(); // modern
list.removeChild(li); // old
Traversing the tree
closest + event delegation: event.target.closest('.btn') finds which button was clicked even if you clicked the icon inside it β this powers the delegation pattern in the next section.Reading layout & geometry
el.getBoundingClientRect(); // {x, y, top, left, right, bottom, width, height}
el.offsetWidth; el.offsetHeight; // rendered size (rounded)
el.scrollLeft; el.scrollTop; // scroll position
window.innerWidth; window.innerHeight; // viewport size
window.scrollY; // how far the page is scrolled
getComputedStyle(el).color; // final computed CSS value
Build a to-do list: an <input>, a <ul> and a Save button. On Save, create an <li> with the text, append it, clear the input, and refocus it. Add a β span in each <li> that removes it when clicked.
09 Events & Event Delegation Intermediate
An event is a signal that something happened: a click, a keypress, a page load. You attach a listener function and the browser calls it when the event fires.
Adding & removing listeners
button.addEventListener('click', (event) => {
console.log('clicked!', event.target); // event carries rich data
});
// Options object
button.addEventListener('click', handler, { once: true }); // auto-removes
button.addEventListener('scroll', handler, { passive: true }); // never blocks
button.addEventListener('keydown', handler, { capture: true }); // ancestor phase
// Remove β must be the SAME function reference
button.removeEventListener('click', handler);
addEventListener('click', () => work()) then later removeEventListener('click', () => work()) does nothing β they are different objects. Store the function in a variable and use it for both add and remove.The event object
form.addEventListener('submit', (e) => {
e.preventDefault(); // stop page reload β essential
const data = new FormData(e.target);
console.log(data.get('email'));
});
input.addEventListener('input', (e) => { // fires on every keystroke
console.log(e.target.value);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submit();
if (e.key === 'Escape') close();
if (e.ctrlKey && e.key === 's') { e.preventDefault(); save(); }
});
Bubbling & delegation
Clicks on a child bubble up through its ancestors. Instead of one listener per item (slow with thousands of rows), attach one listener to the container:
// β
ONE listener for the whole list β works for items added later too
list.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="delete"]');
if (!btn) return; // clicked elsewhere
btn.closest('li').remove();
});
Common events cheat sheet
window.addEventListener('DOMContentLoaded', init);
// resize / scroll β throttle with rAF or a debounce
window.addEventListener('resize', debounce(() => relayout(), 150));
Build an accordion FAQ with one delegated click listener on the container. Add a "collapse all" button that works even though the items were created after page load.
10 Async JavaScript, Promises & Fetch Advanced
JavaScript is single-threaded: one thing at a time. Network requests and timers must not freeze the page, so they run "in the background" and JS continues. When they finish, the event loop runs your callback. This model is called non-blocking I/O.
Callbacks β Promises β async/await (the evolution)
// 1. Callback (old style β "callback hell" with nesting)
getUser(id, (err, user) => {
if (err) return handle(err);
getPosts(user, (err, posts) => {
// nested, hard to read, hard to error-handle
});
});
// 2. Promise β a future value with .then chaining
getUser(id)
.then(user => getPosts(user))
.then(posts => render(posts))
.catch(err => handle(err)) // single place for ALL errors
.finally(() => hideSpinner());
// 3. async/await β looks synchronous, stays non-blocking (preferred)
try {
const user = await getUser(id);
const posts = await getPosts(user);
render(posts);
} catch (err) {
handle(err);
} finally {
hideSpinner();
}
await pauses that async function only β the rest of the page keeps running.Creating promises
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function retry(fn, tries = 3) {
for (let i = 0; i < tries; i++) {
try { return await fn(); }
catch (e) { if (i === tries - 1) throw e; await wait(2 ** i * 500); }
}
}
Parallel vs sequential
// β Sequential β waits for each (slow: 1+1 = 2s)
const u = await fetchUser();
const p = await fetchPosts();
// β
Parallel β start both together (1s total)
const [u, p] = await Promise.all([fetchUser(), fetchPosts()]);
// First to finish wins (with a timeout)
const fast = await Promise.race([fetchFromA(), fetchFromB()]);
// All settle regardless of rejection
const results = await Promise.allSettled([fetchUser(), fetchPosts()]);
results.forEach(r => r.status === 'fulfilled' ? r.value : r.reason);
// Run N tasks at a time (concurrency limit)
await Promise.all(urls.map(u => fetch(u))); // all at once
await outside an async function β SyntaxError; (2) forgetting await β you pass a Promise around instead of its value, and errors silently don't reach your catch.fetch β talking to APIs
// GET
const res = await fetch('https://api.example.com/users?limit=10');
if (!res.ok) throw new Error(`HTTP ${res.status}`); // 404/500 do NOT throw!
const users = await res.json();
// POST with JSON body
const created = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Laksh', role: 'admin' }),
}).then(r => { if (!r.ok) throw new Error('POST failed'); return r.json(); });
// DELETE / PUT
await fetch(`/api/users/${id}`, { method: 'DELETE' });
// Form data (uploads)
const form = new FormData(fileForm);
await fetch('/upload', { method: 'POST', body: form }); // boundary auto-set
fetch only rejects on network failure (offline, DNS). A 404 or 500 resolves normally with res.ok === false β you must check it yourself or errors vanish.AbortController & timeouts
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 5000); // 5s timeout
try {
const res = await fetch('/api/slow', { signal: ctrl.signal });
} catch (err) {
if (err.name === 'AbortError') console.log('timed out or cancelled');
}
// Cancel a stale request when the user types again
let current;
async function search(q) {
current?.abort();
current = new AbortController();
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`,
{ signal: current.signal });
return res.json();
}
AbortController with a debounce (fire only after typing pauses) for search-as-you-type boxes. Without both, you'll render results from an old request over newer ones.11 Errors & Debugging Intermediate
try / catch / finally & throwing
try {
const data = JSON.parse(raw);
if (!data.id) throw new Error('Missing id'); // your own errors
} catch (err) {
console.error(err.message);
} finally {
hideSpinner(); // ALWAYS runs β cleanup code
}
// Custom error class β better stack traces & typed handling
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
throw new ValidationError('Email is invalid', 'email');
Error properties
try { risky(); } catch (err) {
err.message // human-readable text
err.name // "TypeError"
err.stack // file:line:col chain β the debugging gold mine
}
Debugging techniques
console.log('value:', value); // basics still work
console.table(rows); // arrays of objects β readable table
debugger; // programmatic breakpoint (DevTools open)
// debugger; β remove or guard before shipping
// Source of a bug: log with context, not alone
console.assert(user.id, 'user missing id', user);
await with no try/catch (and no .catch()) crashes to the console as an unhandled promise rejection. In Node, that can exit the process. Catch at the boundary where you can inform the user.12 Modules β Splitting Code Across Files Advanced
Modules let you split a program into files with explicit imports/exports. Each module has its own scope β no accidental globals, no load-order puzzles.
Exporting
// utils.js
export const PI = 3.14159; // named export (preferred)
export function clamp(n, min, max) {
return Math.min(Math.max(n, min), max);
}
export default function greet(name) { return `Hi ${name}`; } // one per file
// Or group at the end
function a() {} function b() {}
export { a, b, a as helperA };
// Re-export (barrel files)
export * from './math.js';
export { clamp } from './utils.js';
Importing
import greet, { clamp, PI } from './utils.js'; // default + named
import { clamp as limit } from './utils.js'; // rename
import * as utils from './utils.js'; // namespace
import './side-effect.css'; // side effects only
// Dynamic import β load on demand (code-splitting)
const { renderChart } = await import('./chart.js');
Using modules in HTML
<script type="module" src="app.js"></script>
<!-- type="module" implies defer, runs in strict mode,
and URLs need explicit ./ or full paths -->
if (use dynamic import()), (5) each module = one HTTP request β bundlers (Vite/webpack) merge them for production.features/auth/{api.js, validators.js, LoginPage.js} keeps related code together. Export small, focused functions with clear names; a module that exports 40 things is doing too much.13 Modern JS β ES6+ Features You'll Use Daily Intermediate
Template literals & tagged templates
const name = 'Laksh', points = 42;
const msg = `Hello ${name}! You scored ${points}.
Runs across lines too.`;
// String methods
msg.startsWith('Hello'); msg.endsWith('too.'); msg.includes('scored');
'abc'.repeat(3); // "abcabcabc"
' hi '.trim(); // "hi"
'-'.padStart(8, '-'); // "-------"
Optional chaining & nullish coalescing
const city = user?.address?.city ?? 'Unknown';
// ^^^^^ chain: undefined anywhere β undefined (no crash)
// ?? only falls back on null/undefined
user?.save?.(); // call only if save exists
arr?.[0] // first element if arr exists
data?.name?.[0]
// β Difference from ||
0 ?? 'fallback' // 0 β 0 is NOT nullish
0 || 'fallback' // "fallback" β 0 is falsy
// Use ?? when 0 and '' are VALID values (counts, search boxes)
Symbols, iterators & generators (conceptual)
// Generators β functions that can pause/resume (yield)
function* range(start, end) {
for (let i = start; i < end; i++) yield i;
}
[...range(0, 5)] // [0,1,2,3,4] β no giant array in memory
// Iterables: anything with Symbol.iterator (arrays, strings, Map, Set)
for (const ch of 'hi') console.log(ch); // "h", "i"
Map & Set vs plain objects
// Map β any key type, remembers insertion order
const cache = new Map();
cache.set(userId, userData); // keys are NOT stringified
cache.get(userId);
cache.has(userId); cache.delete(userId);
cache.size;
for (const [key, val] of cache) { β¦ }
// Set β unique values only
const unique = new Set([1, 2, 2, 3]); // {1,2,3}
unique.add(4); unique.has(2); unique.delete(1);
const arr = [...new Set(ids)]; // dedupe an array β classic trick
Map for dynamic keys, frequent add/remove, or when you need insertion order guaranteed; Set for membership tests and dedupe (O(1) lookups).Class syntax (syntactic sugar over prototypes)
class User {
static count = 0; // shared by the class itself
constructor(name, role = 'user') {
this.name = name;
this.role = role;
User.count++;
}
greet() { return `Hi, I'm ${this.name}`; }
get initials() { return this.name[0]; } // read like a property
}
class Admin extends User {
constructor(name) { super(name, 'admin'); } // must call super() first
ban(user) { β¦ }
}
const u = new User('Laksh');
u.greet(); u.initials; User.count;
Object.groupBy(arr, fn) (ES2024), Array.prototype.toSorted/toReversed/toSpliced (non-mutating), Promise.withResolvers() (ES2024), and using declarations for automatic resource cleanup. Check MDN for browser support before using very new features.14 Web Storage & State on the Client Beginner
// localStorage β persists forever, strings only, ~5 MB per origin
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // "dark" or null
localStorage.removeItem('theme');
localStorage.clear();
// Objects must be serialized
localStorage.setItem('user', JSON.stringify(user));
const u = JSON.parse(localStorage.getItem('user') ?? '{}');
// sessionStorage β same API, cleared when the tab closes
sessionStorage.setItem('step', '2');
JSON.stringify/parse; (2) never store passwords/tokens meant to be secret β anything JS can read, scripts can read (an XSS bug exposes it); (3) getItem returns null when missing, and JSON.parse(null) doesn't throw β but JSON.parse('garbage') does. Wrap parses in try/catch.// Safe helpers
function loadJSON(key, fallback) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : fallback;
} catch {
return fallback; // corrupted entry
}
}
function saveJSON(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { /* quota */ }
}
Build a light/dark toggle that saves the choice with localStorage and applies it on page load before first paint (inline script in <head> to avoid a flash β exactly what this guide's own theme script does).
15 Timers, Browser APIs & Performance Intermediate
setTimeout & setInterval
const timerId = setTimeout(() => showPopup(), 3000); // once, after 3s
clearTimeout(timerId);
const pollId = setInterval(pollStatus, 5000); // every 5s
clearInterval(pollId); // stop!
// Chaining instead of setInterval (guarantees no overlap)
function poll() {
check().finally(() => { timeoutId = setTimeout(poll, 5000); });
}
// Debounce
function debounce(fn, ms = 300) {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}
// Throttle (leading edge)
function throttle(fn, ms = 100) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= ms) { last = now; fn(...args); }
};
}
// requestAnimationFrame β schedule work before the next repaint (60fps)
function onScroll() {
requestAnimationFrame(() => { parallaxY = window.scrollY * 0.5; });
}
Useful browser APIs
// Copy to clipboard (secure context required)
await navigator.clipboard.writeText(text);
// Lazy-load with IntersectionObserver
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.src = e.target.dataset.src; io.unobserve(e.target); }
});
}, { rootMargin: '200px' });
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
// Respect user preferences
if (matchMedia('(prefers-reduced-motion: reduce)').matches) disableAnimations();
querySelector results; (3) build lists with one innerHTML write instead of thousands of append calls (or use DocumentFragment); (4) measure first: DevTools Performance tab shows real jank.16 Best Practices & Common Pitfalls Advanced
Style & correctness
// β Deep nesting
function process(order) {
if (order) {
if (order.paid) {
if (order.items.length) { β¦ }
}
}
}
// β
Guard clauses β flatten
function process(order) {
if (!order) return;
if (!order.paid) return;
if (!order.items.length) return;
β¦
}
The 10 classic pitfalls
- Missing
===βif (value = 10)assigns instead of compares β infinite loop - Forgetting
e.preventDefault()on form submit β page reloads, state lost thislost in callbacks β arrow functions orbindwhen passing methods- Async mistakes β missing
await, orawaitin a loop when calls are independent fetchwithoutres.okcheck β 500s look like successinnerHTMLwith user data β XSS vulnerability- Mutating shared arrays/objects β UI doesn't update or state corrupts
- Stale closures in loops β
var icaptured by all callbacks; uselet - Memory leaks β listeners never removed, timers never cleared, detached DOM retained by closures
- Using
== nullonly β otherwise strict;parseIntwithout radix (pass 10)
// Stale closure trap β all callbacks print 3, 3, 3 (with var)
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i), 0);
// Fixed β let creates a new binding each iteration
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i), 0); // 0 1 2
Tooling every serious project uses
// npm scripts worth having
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint src --max-warnings 0",
"test": "vitest run",
"format": "prettier --write ."
}
17 Practice Projects Build to Learn
Work up the ladder β each project reuses concepts from earlier sections. Build them in vanilla JS first; frameworks later.
Starter skeleton β anything interactive
// app.js
'use strict';
const state = { items: [], filter: 'all' };
function init() {
loadState();
bindEvents();
render();
}
function bindEvents() {
document.querySelector('#add').addEventListener('click', addItem);
document.querySelector('#list').addEventListener('click', onListClick); // delegated
}
function render() {
const list = document.querySelector('#list');
list.innerHTML = state.items.map(itemHTML).join(''); // one write
}
document.addEventListener('DOMContentLoaded', init);
state object β change state β call render() to redraw from state. This unidirectional flow is exactly what React/Vue do under the hood β you're already "thinking in frameworks".