Variables, functions, arrays, and conditionals — the fundamentals of JS.
JavaScript has three ways to declare a variable: let and const (both block-scoped, added in ES6) and the older var (function-scoped, best avoided in new code). Use const by default — it signals the binding won't be reassigned — and reach for let only when a value genuinely needs to change.
Every value has one of a small set of primitive types: string (text), number (both integers and decimals — JavaScript has no separate int type), boolean (true/false), undefined (a declared-but-unassigned variable), and null (an intentional "no value"). Anything more complex — arrays, functions, plain objects — is technically an object.
The typeof operator returns a string naming a value's type, handy for quick debugging — though it has a famous quirk: typeof null returns "object", a bug preserved for backwards compatibility since JavaScript's earliest versions.
const name = "Ada";
let age = 30;
age = 31; // let can be reassignedtypeof "hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"Arithmetic operators (+ - * / %) work as expected, with % (modulo) returning the remainder of a division — useful for checking even/odd numbers or wrapping a value around a range.
Comparison has two flavors: == checks equality after converting both sides to the same type ("5" == 5 is true), while === checks equality without converting anything ("5" === 5 is false). Prefer === almost everywhere; it avoids surprising type-coercion bugs.
Logical operators && (AND), || (OR), and ! (NOT) combine boolean expressions. Both && and || short-circuit — they stop evaluating as soon as the result is determined, which is a common way to write default values or guard conditions.
"5" == 5; // true (converts types)
"5" === 5; // false (no conversion)const nameToShow = userName || "Guest";A function is a reusable block of code. Function declarations (function name() {}) are hoisted — usable before their definition in the file — while function expressions and arrow functions are not.
Arrow functions (const fn = () => {}) are a shorter syntax popular for callbacks. Beyond brevity, they also don't have their own this binding, inheriting it from the surrounding scope instead — a detail that matters once you're writing methods on objects or classes.
Parameters can have default values (function greet(name = "friend")), and the rest parameter (...args) collects any number of extra arguments into a real array.
const greet = (name = "friend") => `Hello, ${name}!`;function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}An array is an ordered list of values, created with square brackets: const fruits = ["apple", "banana"]. Items are accessed by zero-based index — fruits[0] is "apple" — and .length always reflects the current item count.
Arrays come with mutating methods that change the array in place — push/pop add or remove from the end, shift/unshift do the same at the start — and non-mutating methods that return a new array or value without touching the original, like the transformation methods covered next.
const fruits = ["apple", "banana"];
fruits.push("cherry");
fruits.length; // 3fruits.includes("banana"); // trueThese methods cover almost everything you'll do with a list of data, and none of them mutate the original array. forEach runs a function on every item purely for its side effects (like logging) and returns undefined.
map transforms every item and returns a new array of the same length — the tool for "I have a list of X and want a list of Y". filter keeps only the items that pass a test and returns a shorter (or equal-length) array.
reduce is the most general: it walks the array accumulating a single result, driven by a callback that receives the running total and the current item. Anything map or filter can do, reduce can also do — but map/filter are usually clearer when they fit.
const nums = [1, 2, 3, 4, 5];
const evenDoubled = nums.filter(n => n % 2 === 0).map(n => n * 2);
// [4, 8]const total = nums.reduce((sum, n) => sum + n, 0); // 15An object groups related data as key-value pairs: const user = { name: "Ada", age: 30 }. Properties are read with dot notation (user.name) or bracket notation (user["name"]) — bracket notation is required when the key is dynamic or not a valid identifier.
Object.keys(), Object.values(), and Object.entries() turn an object's keys, values, or [key, value] pairs into arrays, which is how you loop over an object using array methods like map or forEach.
Methods are just functions stored as properties. Inside a regular function method, this refers to the object the method was called on.
const user = { name: "Ada", age: 30 };
Object.entries(user).forEach(([key, value]) => console.log(key, value));const counter = {
count: 0,
increment() { this.count++; },
};if/else if/else branches execution based on a condition. Any value can be used as a condition — JavaScript coerces it to true or false, and the "falsy" values worth memorizing are false, 0, "", null, undefined, and NaN. Everything else, including "0" and empty arrays/objects, is truthy.
The ternary operator (condition ? valueIfTrue : valueIfFalse) is a compact if/else for a single expression, most useful when assigning a value rather than running a whole block of statements.
switch compares one value against several possible cases and reads more cleanly than a long if/else if chain when you're checking the same variable repeatedly. Each case needs a break, or execution "falls through" into the next one.
const status = age >= 18 ? "adult" : "minor";switch (day) {
case "Sat":
case "Sun":
console.log("Weekend");
break;
default:
console.log("Weekday");
}for (let i = 0; i < n; i++) is the classic counting loop, giving full control over the start, condition, and step. It's the right tool when you need the index itself, not just each value.
for...of iterates the values of anything iterable — arrays, strings, Maps, Sets — and is usually what you want for "do something with each item". for...in iterates the enumerable keys of an object instead, and is rarely used on arrays since it also picks up inherited/extra properties.
while and do...while loops run based on a condition rather than a count, useful when you don't know in advance how many iterations you'll need.
for (const fruit of ["apple", "banana"]) {
console.log(fruit);
}let n = 10;
while (n > 0) {
n = Math.floor(n / 2);
}Strings are immutable — every string method returns a new string rather than changing the original. Useful ones include .length, .slice(start, end), .toUpperCase()/.toLowerCase(), .includes(substring), .trim(), and .split(separator) to turn a string into an array.
Template literals, written with backticks, allow ${...} interpolation directly inside the string and support real multi-line text without \n escapes — both things regular "..." strings can't do.
" Hello World ".trim().toLowerCase();
// "hello world"const name = "Ada";
const greeting = `Hello, ${name}! You have ${3 + 2} messages.`;Destructuring unpacks values from arrays or properties from objects into individual variables in one line: const [first, second] = list, or const { name, age } = user. Object destructuring can also rename and default: const { name: userName = "Guest" } = user.
The spread operator (...) expands an array or object into individual elements — useful for copying (const copy = [...original]), merging ({ ...defaults, ...overrides }), or passing an array as individual function arguments (Math.max(...numbers)).
const { name: userName = "Guest" } = user;const settings = { ...defaults, ...userSettings };try/catch lets you run code that might fail without crashing the whole program: code that throws inside the try block jumps straight to catch, where you get the error object.
throw new Error("message") creates and immediately throws an error — the standard way to signal something has gone wrong inside a function. finally runs a block of cleanup code whether or not an error occurred.
try {
JSON.parse("not valid json");
} catch (err) {
console.log(err.message);
}function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}Scope determines where a variable is visible. let and const are block-scoped (visible only inside the {} they're declared in), while var is function-scoped — one of the main reasons let/const are preferred today.
A closure is a function that "remembers" the variables from the scope it was created in, even after that outer function has finished running. This is how you build private state in JavaScript without classes.
if (true) {
let x = 1;
}
// x is not accessible herefunction makeCounter() {
let count = 0;
return () => ++count;
}
const counter = makeCounter();
counter(); // 1
counter(); // 2