Chapter 2: JavaScript Foundations
Every programming language is built on a set of core ideas that define how its code is written, read, and executed. Before learning about data types, functions, or objects, it is important to understand the basic structure of the language itself. This chapter explores the essential building blocks that form the foundation of all JavaScript programs.
Here you will learn what statements and expressions are, how JavaScript uses tokens and identifiers to form valid code, and how comments can make your programs easier to read. You will also see how JavaScript organises code into blocks and manages scope, and how its automatic semicolon insertion feature can sometimes change the way your code behaves.
These topics may seem simple, but together they define how the language interprets your instructions. A solid understanding of them helps you read other people’s code, avoid subtle syntax errors, and write your own programs with greater precision.
JavaScript’s foundations are not complicated, yet they contain details that matter. By mastering them early, you will gain a clear sense of how the interpreter works and how your code becomes a sequence of actions that the engine performs step by step.
Statements and expressions
Every JavaScript program is built from statements and expressions. They are the smallest meaningful units of execution in the language, and understanding the distinction between them helps you predict how code behaves and where certain constructs are valid.
Expressions
An expression is a fragment of code that produces a value. It can be as simple as a literal number or string, or as complex as a function call or logical comparison. When the JavaScript engine evaluates an expression, it always resolves to a single value.
5 // a literal expression
"Hello" // a string expression
a + b // an arithmetic expression
myFunction() // a function call expression
user.age > 18 // a comparison expression
Expressions can appear anywhere a value is expected. They can be assigned to variables, passed as arguments to functions, or combined with other expressions to create new ones. This makes them flexible building blocks for all JavaScript code.
Statements
A statement performs an action. It may declare a variable, make a decision, start a loop, or call a function. Statements usually end with a semicolon, although JavaScript can sometimes insert them automatically (as explained later in this chapter).
let total = 0; // variable declaration statement
if (total < 10) { // if statement
total++;
}
console.log(total); // expression statement
Some statements, such as if, for, and while, control the flow of a program. Others, like return or break, affect how execution moves within functions or loops.
Expression statements
Many expressions can also be used as standalone statements. When this happens, they are called expression statements. For example, a function call can be both an expression and a statement depending on how it is used:
myFunction(); // expression used as a statement
let result = myFunction();
// same expression used in a larger expression
JavaScript allows expression statements because they often cause useful side effects, such as logging output or modifying data. However, not all expressions can stand alone; for instance, an arithmetic expression like 5 + 2 on its own does nothing unless its result is used.
Building programs from statements and expressions
JavaScript programs are sequences of statements that may contain expressions. When a program runs, the JavaScript engine evaluates each statement in order, computing the results of any expressions as needed. This simple but consistent model makes it easy to reason about the flow of execution.
Tokens, identifiers, and comments
When you write JavaScript code, the interpreter reads it as a stream of small, meaningful elements called tokens. These tokens form the building blocks of the language; names, numbers, symbols, and punctuation that together create statements and expressions. Understanding how JavaScript breaks code into tokens helps explain how it parses and executes your programs.
Tokens in JavaScript
A token is the smallest unit of syntax that carries meaning. Examples include keywords, variable names, operators, and literals. When the JavaScript engine reads your code, it divides it into tokens before interpreting or compiling it.
let total = 5 + 3;
In this line, the tokens are let, total, =, 5, +, 3, and the semicolon ;. The interpreter recognises each as a separate piece of syntax and processes them accordingly.
Identifiers
An identifier is the name you give to something in your code, such as a variable, function, or class. Identifiers must begin with a letter, underscore (_), or dollar sign ($), and can contain digits after the first character. They are case-sensitive, so total and Total refer to different variables.
let name = "Robin";
let Name = "Alex"; // different identifier
Identifiers cannot use JavaScript keywords such as if, for, class, or return. Choosing clear, descriptive names makes code easier to read and maintain. Although technically valid, names like x or data1 rarely explain their purpose well.
userCount or calculateTotal() immediately convey meaning and make code easier to follow.
Unicode in identifiers
JavaScript identifiers can include Unicode characters, meaning you can use letters and symbols from many languages, not just English. While this feature allows flexibility, it is best to use standard ASCII letters and digits for clarity and compatibility.
let café = "open"; // valid, but may confuse readers
let cafe = "open"; // clearer and more common
Comments
Comments are parts of code ignored by the interpreter. They are used to explain what code does, mark sections, or temporarily disable lines during testing. JavaScript supports two styles of comments: single-line and multi-line.
// This is a single-line comment
let total = 5; // You can also comment after a statement
/*
This is a multi-line comment.
It can span several lines and is often used for longer explanations.
*/
Comments do not affect how the program runs, but they greatly improve readability and maintainability, especially in collaborative projects.
Whitespace and readability
JavaScript generally ignores spaces, tabs, and line breaks outside strings. These are used mainly to make code readable. Consistent indentation and spacing help others understand the structure of your code at a glance.
// Easy to read
if (count < 10) {
total += count;
}
// Harder to read
if(count<10){total+=count;}
Clean, well-formatted code is easier to debug and maintain, even though the interpreter does not require strict spacing rules.
Blocks and lexical structure
JavaScript programs are organised into blocks of code that group related statements together. These blocks define the structure of your program, control the visibility of variables, and determine where certain pieces of code can be used. The rules that govern this organisation form JavaScript’s lexical structure.
Blocks
A block is a section of code enclosed in curly braces {…}. It can contain one or more statements and is used in functions, loops, and conditionals to group actions that should execute together.
if (count > 0) {
console.log("Count is positive");
count--;
}
In this example, both statements inside the braces belong to the if block. If the condition is true, the entire block runs; otherwise, it is skipped. Even when a block contains only a single statement, using braces can improve clarity and prevent logical errors when you later expand the code.
Lexical structure
The lexical structure defines how JavaScript code is written and read at the most basic level. It includes the use of tokens, whitespace, comments, line breaks, and punctuation. The interpreter follows these rules to separate and interpret parts of your code correctly.
Because JavaScript is a lexically scoped language, the placement of code within its textual structure determines which variables and functions are visible at any point. This means the physical layout of your code (where you declare things) matters to how they behave at runtime.
Lexical scope
Lexical scope defines where variables can be accessed based on where they are declared in the source code. Each function, block, or module introduces a new scope. Variables declared with let and const are confined to the block in which they appear, while variables declared with var are visible throughout the entire function or global scope.
let outer = "outside";
{
let inner = "inside";
console.log(outer); // accessible
}
// console.log(inner); // ReferenceError: inner is not defined
Here, inner is available only inside its block, but outer is visible everywhere within the same function or script. This is a key part of how modern JavaScript manages variable lifetimes and prevents accidental interference between sections of code.
Nested blocks and shadowing
Blocks can be nested inside one another. When two variables share the same name in different scopes, the inner declaration shadows the outer one, temporarily hiding it within that block.
let message = "outer";
{
let message = "inner";
console.log(message); // "inner"
}
console.log(message); // "outer"
Shadowing is often used intentionally to create temporary variables without affecting the outer scope, but using distinct names can make code easier to follow.
Global scope
Variables declared outside any function or block belong to the global scope. They are accessible from anywhere in your code, including functions and nested blocks. However, overusing global variables can lead to naming conflicts and unintended side effects, especially in large programs.
let version = "1.0";
function showVersion() {
console.log(version); // accessible everywhere
}
In modern JavaScript, the use of modules and block-scoped declarations (let and const) helps reduce reliance on the global scope, keeping programs more predictable and modular.
Together, blocks and lexical structure define the framework that gives meaning and order to JavaScript code. They determine how variables are created, where they exist, and how different parts of a program interact with one another.
Automatic semicolon insertion
JavaScript statements usually end with semicolons, but unlike many languages, the interpreter can automatically insert them when they are missing. This feature, known as Automatic Semicolon Insertion (ASI), was designed to make the language more forgiving and easier for beginners. However, it can also cause unexpected behaviour if you rely on it too much.
When semicolons are automatically inserted
The JavaScript engine inserts semicolons in a few specific situations where it detects the end of a statement. The most common cases are:
- When a line break appears where a semicolon is expected.
- At the end of a block (
}) if no semicolon follows. - At the end of a script or function.
For example, the following code runs correctly even though there are no semicolons:
let a = 5
let b = 10
console.log(a + b)
JavaScript automatically inserts semicolons after each line. While this may seem convenient, the rules behind ASI are subtle, and there are cases where it can change the meaning of your code.
When ASI can cause problems
One of the most common pitfalls occurs when a line begins with an expression that could be interpreted as a continuation of the previous line. In such cases, the missing semicolon can lead to incorrect grouping of statements.
let result = getValue()
[1, 2, 3].forEach(console.log)
Here, JavaScript interprets the code as a single statement because it assumes the array is being accessed from the return value of getValue(). This causes a runtime error. To fix it, insert a semicolon before the array or at the end of the previous line:
let result = getValue();
[1, 2, 3].forEach(console.log);
Return statements and ASI
ASI can also change the behaviour of return statements if you place the return value on the next line. JavaScript automatically inserts a semicolon after return when it encounters a line break, returning undefined instead of the intended value.
function getItem() {
return
{
name: "Book"
}
}
console.log(getItem()); // undefined
To fix this, place the opening brace on the same line as return:
function getItem() {
return {
name: "Book"
};
}
(, [, /, +, or -, ensure the previous line ends with a semicolon. This avoids nearly all ASI-related issues.
Best practices
Most experienced developers include semicolons explicitly. Although modern JavaScript engines handle ASI well, writing them yourself makes your intent unambiguous and prevents subtle bugs. Consistency is more important than the style itself. Whether you use semicolons or omit them, do so the same way throughout your project.
Automatic semicolon insertion was introduced to make JavaScript friendlier to new programmers, but understanding its limits helps you write more reliable and predictable code.
Value vs reference
In JavaScript, the way data is stored and passed between variables depends on whether it is a value type or a reference type. Understanding this distinction is essential for predicting how changes to variables will affect your program. It determines whether copying or modifying one variable also affects another.
Primitive values
JavaScript’s primitive types (number, string, boolean, null, undefined, symbol, and bigint) are always stored and passed by value. This means each variable holds its own independent copy of the data.
let a = 10;
let b = a; // copy the value of a
b = 20;
console.log(a); // 10
console.log(b); // 20
Changing b does not affect a because they store separate values. Each assignment of a primitive creates a new copy in memory.
Reference types
Objects, arrays, and functions are reference types. When you assign one variable to another, you are copying a reference (a pointer) to the same underlying object, not the object itself. Both variables then refer to the same data in memory.
let x = { count: 5 };
let y = x; // copy the reference, not the object
y.count = 10;
console.log(x.count); // 10 — both point to the same object
Because x and y reference the same object, modifying one affects the other. This behaviour often surprises new developers coming from languages that default to value semantics.
Copying reference types safely
To create a real copy of an object or array, you must explicitly duplicate its contents. A common way to do this is by using the spread syntax (...) or functions such as Object.assign().
let original = { a: 1, b: 2 };
let copy = { ...original };
copy.a = 99;
console.log(original.a); // 1
console.log(copy.a); // 99
Here, copy holds a new object with its own data. However, this is a shallow copy: if an object contains other objects, the nested ones are still shared by reference. For deep copying, utility libraries or structured cloning functions can be used.
[...array] or array.slice() to avoid modifying the original.
Comparing values and references
When comparing primitives, JavaScript checks whether the actual values are equal. When comparing objects or arrays, it checks whether the references point to the same memory location.
let m = [1, 2, 3];
let n = [1, 2, 3];
console.log(m === n); // false — different arrays
console.log(m == n); // false — same reason
let p = n;
console.log(p === n); // true — both reference the same array
This distinction explains why two identical objects or arrays are not considered equal unless they are literally the same instance.
When to use each
Primitive values are ideal for simple, independent data. Reference types are suited for collections, structured information, and objects that may be shared or updated. Knowing which kind you are working with helps you manage memory safely and avoid accidental side effects.
By distinguishing between values and references, you gain a clearer understanding of how JavaScript handles data internally. This knowledge will help you reason about your code more confidently as you move on to variables, functions, and more advanced structures in later chapters.