A web page can contain a list of reading sessions, a field containing a number, and a button that performs a calculation. Before connecting those elements to events, we need to understand the language that performs the work. JavaScript values have types, functions create scopes, and array methods make different promises about which values they return and which objects they change.
The examples below calculate with a small reading log. They can be read as independent programs: first predict a result, then trace the operations that produce it. Revisit browsers, servers and web resources if the distinction between JavaScript execution and a displayed document is unfamiliar.
1. Bindings name values
An identifier is case-sensitive: minutes and Minutes name different bindings. Ordinary names can contain letters, digits, underscores and dollar signs, but cannot begin with a digit or use a reserved keyword.
| Declaration | Reassign the binding? | Scope and use |
|---|---|---|
let | Yes | Block-scoped; use when a later step assigns a new value. |
const | No | Block-scoped; the binding must be initialized when declared. |
var | Yes | Function-scoped rather than block-scoped; also permits redeclaration. |
Both let and const must be initialized before they are accessed. Declaring a second binding with the same name in the same lexical scope is an error. A nested block can declare its own binding with that name, which temporarily hides the outer one.
let completed = 2;
completed += 1;
const target = 5;
{
const target = 8;
console.log(target); // 8
}
console.log(completed, target); // 3 5
const fixes the binding, not the contents of an object it names. A constant array can still receive new elements. Assigning the binding to a different array would be a separate operation.
JavaScript is dynamically typed: a let binding can hold a number at one point and a string later. The value has a type; the binding does not impose one fixed value type.
2. Types and conversions determine an expression's meaning
Common primitive values include strings, numbers, booleans, undefined, null and big integers such as 12n. Symbols are another primitive type. Arrays and ordinary objects are objects. typeof reports a value's type, with two historical details to remember: typeof null is "object", and arrays also produce "object"; use Array.isArray(value) to recognize an array.
Numbers use binary floating-point representation. Arithmetic includes +, -, *, /, exponentiation ** and remainder %. Not every decimal fraction is represented exactly. NaN is a numeric value denoting an unsuccessful numeric result; it is not a string. Big integers are a different type and cannot be mixed directly with number operands in ordinary arithmetic.
Worked example
Trace addition and string concatenation
console.log(2 + 3 + "4"); // "54"
console.log("2" + 3 + 4); // "234"
console.log(Number("12") + 3); // 15
console.log(String(12) + 3); // "123"
console.log(Number("later")); // NaN
The additions group left to right. In the first expression, 2 + 3 produces number 5, then adding a string concatenates to produce "54". In the second, the first addition already produces a string, and the next addition concatenates again. Explicit Number and String conversions make the intended operation visible.
=== and !== compare without converting operand types. Thus 3 === "3" is false, while 3 == "3" is true because loose equality permits conversion. For a computation expecting numeric input, convert deliberately and then compare values of the intended type.
Conditions convert values to booleans. The falsy values include false, 0, -0, 0n, "", null, undefined and NaN; other ordinary values are truthy. In particular, "0" and "false" are nonempty strings and are truthy. Boolean(value) exposes this conversion.
Logical && and || short-circuit and return one of their operand values, not necessarily a boolean. !value returns the negated boolean conversion. Bitwise & and | perform bit operations and must not be substituted for the logical operators in conditions.
Checkpoint
Predict values and types
What are the values of "6" + 2, Number("6") + 2, 6 === "6" and Boolean("0")? Which of the first two produces a number?
Solution · Separate conversion from calculation
The values are "62", 8, false and true. Only Number("6") + 2 produces a number. The first expression concatenates; strict equality keeps number and string distinct; a nonempty string is truthy even when its text is "0".
3. Strings are immutable text values
Single and double quotes create string literals. Escape sequences such as \n, \" and \\ represent a newline, a quote and a backslash. Template literals use backticks and ${expression} to insert a computed value.
const raw = " chapter one, chapter two ";
const titles = raw.trim().split(", ");
const message = `${titles.length} chapters`;
console.log(titles); // ["chapter one", "chapter two"]
console.log(message); // "2 chapters"
console.log(titles.join(" / ")); // "chapter one / chapter two"
trim returns a new string without leading and trailing whitespace; it does not alter raw. split is a string method returning an array, while join is an array method returning a string. String indexing starts at zero, but JavaScript's length and index operations count UTF-16 code units. For example, "A".length is 1 while "📘".length is 2. These are not general counts of visible characters.
Regular expressions describe text patterns. In /^[A-Z]{2}[0-9]{2}$/, ^ and $ anchor the complete input, [A-Z]{2} requires two uppercase Latin letters, and [0-9]{2} requires two digits. Its .test("RD24") result is true, while .test("RD240") is false. A pattern is a precise rule for the format it describes; it does not establish facts beyond that rule.
4. Control flow chooses and repeats work
Use if/else for branching and a conditional expression condition ? first : second when choosing one value. switch selects a matching case; break ends that switch. Loops repeat work: while tests before the body, do...while tests afterwards, and for groups initialization, condition and update.
const sessions = [12, 0, 18, 7];
let total = 0;
for (const minutes of sessions) {
if (minutes === 0) continue;
total += minutes;
}
const label = total >= 30 ? "target reached" : "keep reading";
console.log(total, label); // 37 "target reached"
for...of visits iterable values. Here it obtains 12, 0, 18 and 7. continue skips the remainder of the current iteration; break would end the loop entirely. for...in visits enumerable property keys, so it serves a different purpose and is not the default way to read an array's values.
An indexed loop is useful when assigning array elements: sessions[i] = sessions[i] + 1 changes an element. In contrast, assigning minutes = minutes + 1 to a separate let loop binding would only replace that local value, not the array element from which it came.
5. Functions separate inputs, computation and results
Definition
Parameters, arguments and return values
Parameters are the names in a function definition. Arguments are the actual values supplied at a call. return ends the current call and supplies its result. Reaching the end without a return value produces undefined.
function readingTime(pages, minutesPerPage = 2) {
return pages * minutesPerPage;
}
console.log(readingTime(7)); // 14
console.log(readingTime(7, 3)); // 21
function describeSession(label, ...minutes) {
return `${label}: ${minutes.join(", ")}`;
}
console.log(describeSession("week", 8, 11)); // "week: 8, 11"
A missing argument supplies undefined. A default parameter applies when the corresponding argument is omitted or explicitly undefined, not when it is null. A rest parameter gathers remaining arguments into an array and must appear last. Regular functions also have an arguments object; arrow functions do not create their own.
A function declaration can be called before its declaration within the relevant scope. A function expression assigned with const becomes usable after that initialization executes. The following arrow expression computes the same square as a short regular function:
const square = value => value * value;
const squareWithBody = value => {
return value * value;
};
console.log(square(6), squareWithBody(6)); // 36 36
With an expression body, its expression becomes the return value. With braces, write return explicitly when a result is required. Arrow functions also capture surrounding this; regular methods can receive this from their call. They are not interchangeable merely because a short expression looks equivalent.
6. Function values, scope and callbacks
makeAdder names a function value; makeAdder(4) invokes it. A callback is a function passed to another function for that function to invoke. Passing record lets a receiver call it later; passing record() calls it immediately and passes its result instead.
Worked example
Trace a closure through two callback calls
Create an adder whose fixed offset is 4, then use it both directly and as the transformation in a two-step calculation. Track the argument received by each inner call rather than treating the returned function as its numeric result.
function makeAdder(offset) {
return value => offset + value;
}
const addFour = makeAdder(4);
console.log(addFour(9)); // 13
function applyTwice(value, transform) {
return transform(transform(value));
}
console.log(applyTwice(3, addFour)); // 11
The inner function retains access to the offset binding from the call that created it. This combination of a function and access to its surrounding lexical environment is a closure. The parameter value belongs to each inner call. Code outside makeAdder cannot directly access its local offset binding.
makeAdder(4) first returns a function; it does not yet perform an addition. Calling addFour(9) adds 4 to 9 and returns 13. In applyTwice(3, addFour), the inner application transforms 3 into 7, and the outer application transforms 7 into 11. Both callback calls use the same captured offset, but receive different arguments.
Callbacks need not be asynchronous. applyTwice invokes its callback immediately during the same call. Events and timers use callbacks with different invocation timing. Calling a function, passing a function and scheduling a function are distinct operations.
JavaScript passes argument values. When the value is an object reference, the function can use that reference to change the shared object; reassigning the local parameter does not reassign the caller's binding. An immediately invoked function expression, such as (() => { const note = "ready"; return note; })(), creates a local scope and invokes it once.
Checkpoint
Trace a callback and its surrounding scope
What does applyTwice(5, makeAdder(2)) return? How many calls to the returned inner function occur? Why does its offset remain available?
Solution · Follow function creation and invocation
makeAdder(2) creates a function that adds 2. applyTwice calls that function with 5, obtaining 7, and again with 7, obtaining 9. There are two inner calls. The closure retains access to the offset binding from makeAdder(2) after the outer call returns.
7. Methods and generators have distinct call behavior
An object can store functions as methods. In readingLog.describe(), the call supplies readingLog as this for the regular method. A generator function instead creates a generator object whose execution advances with next().
const readingLog = {
title: "Evening reading",
describe() { return this.title; }
};
console.log(readingLog.describe()); // "Evening reading"
function* checkpoints() {
yield 10;
yield 20;
return 30;
}
const steps = checkpoints();
console.log(steps.next()); // { value: 10, done: false }
console.log(steps.next()); // { value: 20, done: false }
console.log(steps.next()); // { value: 30, done: true }
yield pauses a generator call and produces a value. A later next() resumes it. The return value ends the generator and has done: true; it is not another yielded item. This explicit pause/resume model differs from an ordinary function that runs through to its return.
8. Arrays are ordered objects with shared identity
Array indices begin at zero. Elements may have different types, including other arrays and objects. Assignment copies the reference value rather than constructing a new array.
Worked example
Separate aliasing, copying and mutation
const original = [8, 15, 21];
const alias = original;
const copy = [...original];
alias[0] = 10;
copy.push(30);
console.log(original); // [10, 15, 21]
console.log(alias); // [10, 15, 21]
console.log(copy); // [8, 15, 21, 30]
alias and original refer to one array; copy refers to a second array. Spread, slice() and Array.from() make shallow copies: nested object references are still shared. A shallow copy of an array of objects does not duplicate each object.
Rest and spread use the same three-dot spelling in different positions. A rest parameter gathers arguments, while [...first, ...second] spreads iterable values into a new array. Destructuring selects elements into bindings: const [first, ...remaining] = [8, 15, 21] gives first the value 8 and remaining the array [15, 21].
9. Choose operations by mutation and return value
| Operation | Changes the original array? | Returned value |
|---|---|---|
slice(start, end) | No | New array from inclusive start to exclusive end. |
splice(start, count, ...items) | Yes | Array of removed elements. |
push(...items) / unshift(...items) | Yes | New length after adding at the end / start. |
pop() / shift() | Yes | Removed last / first value, or undefined if empty. |
reverse() | Yes | The same array with reversed order. |
sort(compare) | Yes | The same array sorted according to the comparison. |
const plan = ["read", "pause", "write"];
const preview = plan.slice(0, 2);
const removed = plan.splice(1, 1, "review", "discuss");
console.log(preview); // ["read", "pause"]
console.log(removed); // ["pause"]
console.log(plan); // ["read", "review", "discuss", "write"]
slice and splice accept negative start positions counted from the end. Their second arguments have different meanings: an ending index for slice, a deletion count for splice. Mixing them changes the question being asked.
Default sort() compares string representations, so [4, 20, 3].sort() produces [20, 3, 4]. For ascending numeric order use (a, b) => a - b. To preserve the original array, sort a shallow copy: [...values].sort((a, b) => a - b).
10. Search, select and transform
indexOf and lastIndexOf return a matching index or -1; their comparisons use strict equality. includes answers whether a value is present and also recognizes NaN. For a predicate, find returns the first matching element or undefined, while filter returns an array of all matches. map transforms each visited element and returns a new array. forEach performs callback work and returns undefined; it does not collect returned callback values.
const entries = [" HTML ", "CSS", "JavaScript"];
const clean = entries.map(title => title.trim());
const longTitles = clean.filter(title => title.length >= 4);
const firstLong = clean.find(title => title.length >= 4);
console.log(clean); // ["HTML", "CSS", "JavaScript"]
console.log(longTitles); // ["HTML", "JavaScript"]
console.log(firstLong); // "HTML"
console.log(clean.indexOf("CSS"), clean.includes("DOM")); // 1 false
For these dense arrays, map preserves the number and order of elements. filter preserves the relative order of retained elements but can shorten the result. Neither method changes this input array by itself. A callback that mutates a shared object can still have side effects, so inspect the callback as well as the method name.
Common mistake
A callback's return value needs a receiver
values.forEach(value => value + 1) computes sums and discards them. It does not replace array elements. Use values.map(value => value + 1) when you want a new array of those sums, or explicitly assign an indexed element when mutation is the intention.
An array-like object has indexed entries and a length but need not expose array methods. Array.from({ 0: "read", 1: "review", length: 2 }) creates a real array. Browser HTMLCollection and NodeList objects are related examples: NodeList has forEach but not array find or filter; HTMLCollection does not supply those array methods. querySelectorAll returns a static NodeList, while other NodeLists can be live. A collection's behavior depends on the API that created it, not merely on having a length property.
Exercises
1. A binding and its object
Explain why const sessions = [10]; sessions.push(15); is valid, but subsequently assigning sessions = [20] is not. Would const copy = [...sessions] make nested objects independent?
Solution · Solution 1: Binding stability and shallow copies
push changes the existing array; it does not reassign the sessions binding. The later assignment would reassign a constant binding. Spread makes a different outer array, but its elements retain their values, including references to nested objects. Those nested objects are not independently copied.
2. A missing return
Predict const lengths = ["notes", "web"].map(text => { text.length; });. Rewrite only the callback so the result contains the two lengths.
Solution · Solution 2: Expression bodies and block bodies
The result is [undefined, undefined]: the block body has no return statement. Either text => text.length or text => { return text.length; } gives [5, 3]. map collects each callback's returned value, not the value of an expression that the callback discards.
3. Trace array changes
Starting with const tasks = ["read", "write", "review"];, execute const saved = tasks.slice(1); then const taken = tasks.splice(1, 1, "discuss");. State all three arrays afterwards.
Solution · Solution 3: A copy and a later edit
saved is ["write", "review"], taken is ["write"], and tasks is ["read", "discuss", "review"]. slice created a separate outer array before splice changed tasks; these string elements are values, so the later replacement does not change saved.
4. Define a callback pipeline
For [9, 24, 15, 6], produce a new array containing only values at least 10, with each retained value divided by 3. Preserve the original order and array. Give the result and explain why the order of the two operations matters.
Solution · Solution 4: Select before transforming
[9, 24, 15, 6].filter(value => value >= 10).map(value => value / 3) gives [8, 5]. Selection tests the original values. Dividing first and then applying the same threshold of 10 would test different quantities and give an empty result. Both methods return new arrays here.
5. Resume a generator
After the three shown calls to steps.next() in the checkpoints example, what would one more call return? Is 30 a yielded item?
Solution · Solution 5: Completion is persistent
The next call returns { value: undefined, done: true }. The generator has completed and does not restart. The value 30 was the final return value, with done: true, rather than a value produced by yield.