HTML is structure, CSS is looks β JavaScript is behavior. It's a real programming language: it can store values, make decisions, react to clicks, and change the page live.
"h-e-l-l-o".f(x)). That's exactly what yours do.Every browser has a live JavaScript prompt. Open your page, press F12 (or right-click β Inspect), and click the Console tab. Type code, press Enter, see the result instantly β no files needed to experiment.
undefined after a line is not a failure β it's just what that line
handed back. Don't panic at grey.
A variable is a named box that holds a value. let makes a new one; =
stores a value in it.
let bookTitle = "The Alchemist";
let copies = 4;
console.log(bookTitle); // The Alchemist
console.log(copies); // 4
console.log(x) prints the contents.let can't create the same name twice in one session. Re-running
let bookTitle = ... throws a red error. Fix: refresh the page (F5)
to reset, then run it once.
| Error | Means | Usual cause |
|---|---|---|
ReferenceError: x is not defined | There's no box called x at all | Typo, or used before you made it |
value is undefined (grey, no error) | The box exists but is empty | let x; with nothing stored |
let p; console.log(p) β grey undefined.
console.log(q) (never declared) β red ReferenceError.
file: "unique security origin" warning, for example, is about how the page was
opened β nothing to do with your typed code. Half of errors aren't what you think.
Every value has a type, and the type decides how it behaves. Check any value with
typeof.
| Type | What | Examples |
|---|---|---|
| String | text, always in quotes | "The Alchemist" |
| Number | numbers, no quotes | 4, 19.99 |
| Boolean | just two values | true, false |
The quotes are the whole difference: "4" is text, 4 is a number. They look
the same but behave differently.
+ trap (very important)The + sign does two different jobs depending on the types:
+ glues (concatenates). Otherwise it does math."4", not 4:
let age = "4"; // from a form β TEXT
console.log(age + 1); // want 5, get "41" π±
The fix (coming later): Number(age) converts text β number, so Number(age) + 1 β 5.
+ trapYou can convert a value from one type to another. Note the capital letter β capitalization matters in JavaScript.
| Command | Does | Example |
|---|---|---|
Number(x) | text β number | Number("4") β 4 |
String(x) | number β text | String(4) β "4" |
let age = "4"; // text (like a form gives you)
console.log(age + 1); // "41" β the bug
console.log(Number(age) + 1); // 5 β convert first, then math
Number("hello") β NaN. It means "I tried to make a number and
failed." Three things to know:
Number(...) was fed junk.NaN + 5 β NaN. One bad value poisons the whole sum.typeof NaN β "number". Yes, "Not a Number" is a number. π
Number(...) return NaN β check for it before trusting the value.
A function is a reusable recipe. Write it once, then call it as many times as you like with different inputs.
greet("Sanjay") β "Hello Sanjay".function lateFine(days) { // βΉ2 per day late
return days * 2;
}
lateFine(5); // 10
let fine = lateFine(5); // store it β fine is 10, ready to use
return vs console.log
console.log(x) | return x | |
|---|---|---|
| Does | Prints for a human to see | Hands the value back to your code |
| Can you reuse the value? | No β it just appeared on screen | Yes β store it, add it, display it |
return hands back undefined:
function lateFine2(days) { console.log(days * 2); }
let f = lateFine2(5); // prints 10, but f is undefined!
Rule: want to use the result later β return it. Just want to peek β console.log it.
if / elseDecisions start with a comparison, which asks a yes/no question and returns a
boolean (true/false).
| Operator | Asks | Example |
|---|---|---|
> < | greater / less than | 5 > 3 β true |
>= <= | greater/less than or equal | 4 >= 4 β true |
=== | exactly equal? (three =) | 4 === 4 β true |
!== | not equal? | 4 !== 5 β true |
= vs ===
One = stores a value (let x = 4). Three ===
compares (x === 4). Mixing them up is a classic bug.
Always use === (not ==) β reason comes later.
An if uses a comparison to choose which block runs:
function bookStatus(copies) {
if (copies > 0) {
return "Available";
} else {
return "Out of stock";
}
}
bookStatus(4); // "Available"
bookStatus(0); // "Out of stock"
copies > 0 is
correct but copies >= 0 would be a bug: 0 >= 0 is
true, so it would say "Available" with zero copies on the shelf. When you write a
comparison, always ask: "what happens right at the edge?" (at 0, at the due date
exactly, at an empty string). One character (> vs >=) is the whole bug.
An array holds an ordered list. Square brackets, comma-separated items.
let books = ["Harry Potter", "The Alchemist", "Atomic Habits"];
books[0], last = books[books.length - 1].| Action | Code | Result |
|---|---|---|
| Read first item | books[0] | "Harry Potter" |
| How many items | books.length | 3 |
| Last item | books[books.length - 1] | "Atomic Habits" |
| Add to the end | books.push("Wings of Fire") | list grows to 4 |
undefined, not an error
With 3 items, books[3] returns undefined β no crash, no warning. So a
mysterious undefined often means "I read past the end of an array." JavaScript won't
tell you; you have to notice.
A for loop walks the whole list automatically, however long it is.
for (let i = 0; i < books.length; i = i + 1) {
console.log(books[i]);
}
Three parts in the ( ), separated by semicolons:
let i = 0 β counter starts at 0 (first index)i < books.length β keep going while this is truei = i + 1 β add 1 after each pass<, not <=
With 3 items, valid indexes are 0, 1, 2. i < length stops at the right place.
i <= length runs one time too many β reads books[3] β prints
undefined at the end. The most famous loop bug there is.
i vs i + 1
Use i to reach into the array (books[i], 0-based).
Use i + 1 only to display a human number (1-based):
for (let i = 0; i < books.length; i = i + 1) {
console.log((i + 1) + ". " + books[i]); // 1. Harry Potter β¦
}
An array is great at "many," but a single thing β a book β has several named parts: title, author, category, copies. An object stores them by name (key) instead of by position.
[ ] vs Object { }
Array = a list, reached by position: book[3].
Object = named parts, reached by name: book.copies.
Square brackets for order; curly braces for structure.
let book = {
title: "Wings of Fire",
author: "A.P.J. Abdul Kalam",
category: "Autobiography",
copies: 3
};
book.title; // "Wings of Fire" β dot + name
book.copies; // 3
// author at [1], copies at [3]
let book = ["Wings of Fire", "Kalam", "Autobiography", 3];
book[3]; // 3 (copies) β
// a teammate inserts the year in the middleβ¦
let book = ["Wings of Fire", "Kalam", 1999, "Autobiography", 3];
book[3]; // "Autobiography" π± β copies silently moved to book[4]
No error. No crash. Every line trusting book[3] is now quietly wrong.
With an object, book.copies keeps working no matter what else you add β because there's
no position to shift. Meaning by name, not by position.
undefined (same idea as out-of-bounds)
book.publisher when there's no publisher key β undefined, not
an error. Ask an object for a name it doesn't have and it calmly shrugs β just like reading past the
end of an array.
=. A book gets borrowed:
book.copies = book.copies - 1; // read it, subtract, write it back β 2
That single line is the seed of a real "Borrow" feature.
A library is many structured things, so you combine both tools: a list whose every slot holds an object. This is the single most common data shape on the web β it's what most APIs hand back.
let books = [
{ title: "Harry Potter", author: "J.K. Rowling", category: "Fantasy", copies: 5 },
{ title: "Wings of Fire", author: "A.P.J. Abdul Kalam", category: "Autobiography", copies: 3 },
{ title: "The Alchemist", author: "Paulo Coelho", category: "Fiction", copies: 4 }
];
books.length; // 3
books[0]; // the whole first object
books[1].author; // "A.P.J. Abdul Kalam" β index first, THEN dot
books[1] hands you an object; then .author reaches inside
that object. Index picks the book, dot picks the field. One step at a time.
When the browser loads your HTML, it builds a live tree of objects in memory β one
object per element. That tree is the DOM (Document Object Model). Your
<h1>, your <table> β each is now an object JavaScript can grab
and change, and the browser instantly redraws.
book.copies.
You already know how to work with objects; now you're working with the page as objects.
| Tool | Does |
|---|---|
document | the whole page as an object β the entry point |
document.getElementById("x") | grab the one element whose id is "x" |
.innerHTML | the HTML inside an element β read it, or write it to change the page |
document.getElementById("mainHeading").innerHTML = "Sanjay's Library";
// the <h1 id="mainHeading"> changes on screen instantly
id? A unique handle
To grab one specific element out of hundreds, JS needs a unique name tag. That's the whole
job of the id attribute β a unique handle so getElementById finds exactly one.
.html file on disk never
changed, so refresh rebuilds the DOM fresh from the file. To make a change stick, the data
it comes from has to live somewhere permanent β a file, a database. (Beginners constantly think
refresh keeps DOM edits. It doesn't.)
Now snap it all together: array of objects (data) + for loop (visit
each) + string building (make a <tr>) + innerHTML
(inject). Delete the hand-typed rows β JavaScript generates them.
// empty container in the HTML: <tbody id="bookRows"></tbody>
let rowsHTML = ""; // start empty (accumulator)
for (let i = 0; i < books.length; i++) {
rowsHTML = rowsHTML + // append one <tr> per book
"<tr><td>" + books[i].title +
"</td><td>" + books[i].author +
"</td><td>" + books[i].category +
"</td><td>" + books[i].copies + "</td></tr>";
}
document.getElementById("bookRows").innerHTML = rowsHTML;
for (i = 0; β¦) with no let β it "works," but silently
leaks i as a global variable, and in strict mode throws
ReferenceError outright. Always declare the counter: for (let i = 0; β¦).
It's scope safety, not style.<script> at
the end of <body>. If it runs before <tbody> is
built, getElementById returns null and you get
Cannot set properties of null. The fix is order: element first, script after.An event is something the user does β a click, a keypress. onclick says
"when this is clicked, run this code." That's how a page stops being a poster and becomes an app.
<button onclick="borrowBook(0)">Borrow</button>
borrowBook, which
lowers copies in the array, then calls renderBooks() to redraw. The screen is
always just a picture of the current data. Every button you ever wire up is this same loop.
onclick='borrowBook(${i})' β so row 0's
button becomes borrowBook(0), row 1's borrowBook(1), etc. Each button carries
which book it controls, frozen in at render time. View the page source and you'll see the
numbers sitting there.
books[i].copies changes the data, but the screen won't know until you
call renderBooks() again. Forget the re-render and the number "won't change" even though
the data did. Always: change β re-render.
.value + Number()To read what a user typed, grab the input and read its .value (for an <h1>
you read .innerHTML; for an <input> you read .value).
let title = document.getElementById("newTitle").value;
let copies = Number(document.getElementById("newCopies").value); // convert!
books.push({ title: title, author: author, category: category, copies: copies });
Number()" β the accident trap
.value is always a string, even from type="number". Skipping
Number() can seem fine β because -/*// force
numbers ("3" - 1 = 2). But + is the trap: "3" + 1 = "31". So a sum
of copies silently turns to garbage. "Works by accident" is the most dangerous bug.
Convert input the moment it enters your program, and it behaves with every operator.
Backticks ` let you drop values straight into a string with ${ }, instead of
gluing pieces with +. It reads like the final result, with fill-in blanks.
// old β bounce between quotes and +
"<td>" + book.title + "</td><td>" + book.copies + "</td>"
// template literal β reads like the output
`<td>${book.title}</td><td>${book.copies}</td>`
~).${ } is "live" β JS evaluates inside it (a variable, book.title, even ${2 + 2} β 4). Everything else is literal text.<td> ${book.title} </td> puts spaces around every cell's text. Harmless, but
<td>${book.title}</td> is cleaner. Also: rowsHTML += x is shorthand
for rowsHTML = rowsHTML + x.
forEach β let the array walk itselfA for loop makes you manage a counter (i = 0, i < length,
i++) β pure bookkeeping, and where off-by-one bugs live. forEach does the
walking; you just say what to do per item.
books.forEach(function (book, i) {
// 'book' is the current object, 'i' is its index
console.log(book.title);
});
forEach a function, and it calls your function once per item,
passing in the item (book) and its index (i). No counter, no
books[i], no way to run one-too-many times β the whole off-by-one class of bug disappears.
You describe what to do, not how to count (that's called declarative code).
localStorage β memory that survives refreshEarlier you learned DOM changes vanish on refresh because they live only in memory. localStorage
is a small box the browser keeps on disk, per site β write to it and it survives
refresh, closing the tab, even a reboot.
// save (after every change):
localStorage.setItem("libraryBooks", JSON.stringify(books));
// load (on startup, BEFORE the first render):
let saved = localStorage.getItem("libraryBooks");
if (saved) { books = JSON.parse(saved); }
JSON.stringify / JSON.parse?
localStorage only stores strings. Your books is an
array-of-objects. JSON.stringify turns the whole structure into one text string (to save);
JSON.parse turns it back into a real array (to load). JSON = JavaScript
Object Notation β the standard text format for structured data (it's also what APIs send).
if (saved) guard β the empty first run
On the very first visit nothing is saved, so getItem returns null.
JSON.parse(null) would blow up your app. So load only if something was saved;
otherwise keep your default (seed) books. Boundary thinking again: handle the empty first case.
books array is just seed data β used
only on a fresh browser. After that, the load step overwrites it with saved data every visit. To reset,
run localStorage.clear() in the console and the seed reappears.
rowsHTML β outside the loop: it must remember and grow across all rows (accumulate).rowClass β inside the loop: it must be fresh each row; this book's low-stock has nothing to do with the last. Declared outside, it kept the previous value (or started undefined) β broken rows.localStorage.removeItem(...) left in the file ran on every load and wiped saved
data β so persistence broke. The tell-tale "takes two refreshes" lag came from order:
the code read storage at the top but deleted it at the bottom, so each load showed the
previous load's data, then wiped it. Lesson: scratch/test code (a
clear(), a console.log) belongs in the console, not committed in the file β
clean it up.
An arrow function is a shorter way to write a function. Same job, less typing β and it's what you'll see in every modern codebase, tutorial, and API doc.
// the long way you learned first:
books.forEach(function (book) {
console.log(book.title);
});
// the arrow way β drop 'function', add => after the ( ):
books.forEach((book) => {
console.log(book.title);
});
function hundreds of times a day got old, so 2015's update
added =>. Read it as "goes to": book goes to⦠console.log its title.
{ } AND the
return β the result returns automatically:
// long:
books.filter(function (b) { return b.copies <= 2; });
// one-liner β no braces, no return, same result:
books.filter((b) => b.copies <= 2);
Gotcha: if you keep the { }, you must keep return too.
(b) => { b.copies <= 2 } returns undefined β braces mean "full body,
return it yourself."
this β the hardest keyword, tamedthis means "the object I'm currently working for." The confusion: normal
functions and arrow functions get their this from opposite places.
Normal function | Arrow => | |
|---|---|---|
Where this comes from | Whoever CALLS it β a fresh this per call | Has NO this of its own β looks outward, like any variable |
| Mental model | "Who's calling me right now?" | "What was this where I was written?" |
let myBook = {
title: "Harry Potter",
normalShout: function () {
setTimeout(function () {
console.log(this.title); // undefined π± β setTimeout calls it, not myBook
}, 1000);
},
arrowShout: function () {
setTimeout(() => {
console.log(this.title); // "Harry Potter" β
β arrow looks outward to arrowShout's this
}, 1000);
}
};
this goes to its immediate parent" β NO.
A normal function doesn't look outward at all. It gets a brand-new this from
whoever calls it: myBook.normalShout() β this = myBook;
the same function called bare β this = undefined/window. Only the arrow does the
look-outward-through-scopes thing β exactly like looking up an ordinary variable.
books.sort(...) reorders the array, but it doesn't know what "smaller" means for your
objects. You hand it a compare function that takes two items and answers with a number:
| Your function returns | sort concludes |
|---|---|
| negative | a comes first |
| 0 | leave them as-is |
| positive | b comes first |
// numbers: subtraction gives exactly neg/0/pos β the classic trick
books.sort((a, b) => a.copies - b.copies);
// strings can't subtract β localeCompare gives the same neg/0/pos contract
books.sort((a, b) => a.title.localeCompare(b.title));
books.sort(...) line alone β and "nothing happened."
console.log(books.map(b => b.copies)) proved the DATA had sorted; the SCREEN was
still showing the old picture. Same lesson as the very first render:
every function that touches books must end with
saveBooks() β renderBooks(books). That pair is the spine of the app.
push, splice, sort, pop β surgery on books itself. Changed data β must save + re-render.filter, map β a photocopy. books never changed β nothing to save.saveBooks(): filtering changes
what you show, not what you have. And why indexOf(book) keeps
Borrow/Delete correct after sorting β it finds the object by identity, not by old position.
| Tool | Does | Mutates? |
|---|---|---|
books.splice(i, 1) | removes 1 item at position i (your Delete button) | YES |
books.pop() | removes the last item | YES |
books.filter(...) | doesn't remove β returns a new array without the unwanted items | no |
splice(i, 1)
cuts one item out at position i and rejoins the ends β the array closes the gap (positions shift!).
Rule of thumb: splice = surgery on the real array; filter = photocopy with items left out.
let vs var β why we never use varBoth declare variables. The difference is how far they leak:
let lives only inside its { } block; var ignores blocks and
leaks out to the whole function.
if (true) {
var a = 1;
let b = 2;
}
console.log(a); // 1 β var LEAKED out of the block π±
console.log(b); // ReferenceError β let stayed inside β
var is the 1995 original; its leaking caused a generation of silent bugs β remember your
rowClass-outside-the-loop bug? var makes exactly that class of mistake
invisible. let was added in 2015 to fix it.
Modern rule: always let (or const), never var.
You'll only meet var in old code β recognize it, don't write it.
map β transform every item (+ join)forEach just visits each item. map visits AND transforms:
it builds a new array where each item is whatever your function returned.
books.map((b) => b.title);
// ["Harry Potter", "Wings of Fire", "The Alchemist"] β new array, books untouched
books.map((b) => b.copies);
// [5, 3, 4] β handy debug trick: see one field of everything at a glance
map does exactly
that β same length, each item mapped through your function. Array in β array out.
rowsHTML += ...) works, but modern code says it in one thought:
map each book to its <tr> string, then glue the pieces.
let rowsHTML = list
.map((book) => `<tr><td>${book.title}</td>...</tr>`) // array of row-strings
.join(""); // one big string
Why join("")? map hands back an ARRAY of strings, but
innerHTML needs ONE string. join glues the pieces using the separator you
give it β and without "" the default separator is a comma, so you'd see
stray commas between your table rows on screen.
Positions are fragile: sort the array and "book 3" is a different book. Real systems give every record a permanent, unique id and always look things up by it β that's exactly how databases work (you'll meet this again as a primary key).
{ id: 1, title: "Harry Potter", ... }
// buttons carry the id, not the row position:
`<button onclick='borrowBook(${book.id})'>Borrow</button>`
// find the OBJECT by id:
let book = books.find(b => b.id === id);
// find the POSITION by id (when splice needs one):
let i = books.findIndex(b => b.id === id);
books.splice(i, 1);
let newId = Math.max(...books.map(b => b.id)) + 1;
Read inside-out: all ids β biggest β plus one. The ... is the
spread operator: Math.max wants separate numbers, not an array β
Math.max([1,2,3]) β NaN, but Math.max(...[1,2,3]) β 3
(the dots unpack the array into individual values).
book.id β undefined, and
undefined + 1 β NaN. One cause, two symptoms.
Rule: when you change the data's shape, old saved data is now the wrong shape β
clear it (localStorage.clear()) or convert it. Real apps call this a migration.
= vs === bug β you wrote it live
books.find(b => b.id = id) // π± ONE equals: ASSIGNS id to every book it visits
books.find(b => b.id === id) // β
THREE: compares
The single = silently overwrote ids while searching β data corruption,
no error, and find "succeeded" (assignment is truthy). Β§8 warned about this bug; now you've met it.
In a condition, = is almost always a bug.
find needs no "write back"After let book = books.find(...), is book a copy you must save back into
the array? No. It's the same object β book and the array slot
both point at one object in memory.
let book = books.find(b => b.id === 1);
book === books[0]; // true β SAME object, two names
book.copies = 99;
books[0].copies; // 99 β "the array changed" because there is only ONE object
book.copies = book.copies - 1 already changed the book inside
books β a second "write it back" line (books.find(...).copies = book.copies)
just re-finds the same object and hands it a value it already has. Dead code.
indexOf(book) kept working after sorting β it matches by object identity (the pointer), not by contents.Not built into the Library yet β read this so the concept isn't new when you meet it.
Due dates, fines, "borrowed on" β all need the Date object.
let now = new Date(); // this exact moment
now.toISOString(); // "2026-08-25T14:30:00.000Z" β a SAVEABLE string
// date math goes through milliseconds:
let due = new Date();
due.setDate(due.getDate() + 14); // 14 days from now β handles month-ends itself
// how late is a return? subtract β milliseconds β convert:
let msLate = new Date() - due; // negative = not late yet
let daysLate = Math.ceil(msLate / (1000*60*60*24));
new β first time you've seen it: it constructs a fresh object from a blueprint. Date() without new behaves differently (returns a string) β always use new Date().0, December is 11. getMonth() in August β 7. Legendary trap.toISOString() strings; rebuild with new Date(savedString) when you need math. Same shape-mismatch idea as your id migration.Your ids unlock the pattern behind every real system: records that point at other records by id. A loan is not a copy of a book β it's a small object that references one:
let loans = [
{ loanId: 1, bookId: 3, member: "Sanjay", borrowedOn: "2026-08-25Tβ¦", returnedOn: null }
];
// "which book is loan 1 about?" β follow the id:
let loan = loans.find(l => l.loanId === 1);
let book = books.find(b => b.id === loan.bookId); // same find you already use
returnedOn: null = "still out" β the guard-for-empty thinking again.
This exact picture is what SQL databases call a foreign key β when you meet
PostgreSQL in a later phase, you already know the concept.
β Prev: CSS Basics Β· Back to contents Β· Next: TypeScript β