The Only JavaScript Article You'd Ever Need - 1/6

The Foundation Every Developer Needs

The Reality About JavaScript Learning

Let’s address the elephant in the room. You’ve probably worked through dozens of “JavaScript in 24 hours” tutorials that taught you to build todo lists and weather apps, then left you wondering why you still can’t build anything meaningful. These tutorials focus on surface-level syntax while completely missing the deeper concepts that separate competent developers from code copiers.

JavaScript has evolved from a simple scripting language into the backbone of modern software development. It powers everything from the interfaces you use daily to server-side applications, mobile apps, and even desktop software. While incredibly powerful, JavaScript is also notorious for its quirks, historical baggage, and design decisions that can trap inexperienced developers.

This isn’t another surface-level tutorial. This is the SDE’s Missing Manual for JavaScript—the comprehensive foundation you need to build robust, scalable software. We’re not just learning syntax; we’re building the deep understanding that transforms coders into engineers. The difference will become clear as we progress.

Understanding JavaScript’s Role in Modern Development

At its core, JavaScript (JS) is a high-level, interpreted, dynamically-typed programming language that began as a simple tool for making web pages interactive. Originally designed for basic browser scripting, JavaScript was meant to handle simple tasks like form validation and basic animations. Its initial role was exclusively client-side, running in web browsers to manipulate the Document Object Model (DOM) and respond to user interactions.

Today’s JavaScript landscape is dramatically different. With the introduction of Node.js, JavaScript broke free from browser constraints and became a full-stack language. You can now use JavaScript for frontend interfaces, backend servers, mobile applications, desktop software, and even embedded systems. This versatility is why JavaScript knowledge is so valuable—mastering it opens doors to virtually every area of software development.

As we explore JavaScript fundamentals, your primary debugging tool will be the console, specifically console.log(). Think of it as your window into code execution, allowing you to inspect values, track program flow, and understand what’s happening at runtime.

// Open your browser's developer console (F12 or right-click -> Inspect)
// and paste this in
console.log("Hello, JavaScript!");
console.log(1 + 1);
let message = "This is a variable";
console.log(message);

Running JavaScript: From Static to Interactive

You’ve got some JavaScript code, great! But how do you make your web browser actually execute it? There are a few common ways to include JavaScript in your HTML pages:

1. Inline JavaScript (Generally Avoid)

This is where you embed small bits of JavaScript directly within HTML attributes. It’s usually a bad practice for anything more than the simplest examples because it mixes concerns (HTML for structure, JS for behavior) and makes your code hard to read, maintain, and debug.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Inline JS</title>
  </head>
  <body>
    <h1>Inline JavaScript Example</h1>
    <button onclick="console.log('from inline');">Click Me!</button>
  </body>
</html>

When you click the button, check your console logs. The onclick attribute used here is pretty self-explanatory – the JS code inside gets executed when that element is clicked on.

2. Internal JavaScript (<script> tag in HTML)

This is a better approach for simple, page-specific scripts. You place your JavaScript code directly inside <script> tags within your HTML file.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Internal JS</title>
  </head>
  <body>
    <h1>Internal JavaScript Example</h1>
  </body>

  <script>
    // JavaScript code goes here
    console.log("from internal script!");
  </script>
</html>

When you open this HTML file, you’ll see “from internal script” in your browser’s console.

The // symbols you see are comments—notes that JavaScript ignores but help developers understand code. Comments are essential for maintaining code and explaining complex logic to your future self and teammates.

3. External JavaScript File (<script src="..."> tag)

This is the most common and recommended way to include JavaScript. You write your JavaScript code in a separate file (e.g., script.js) and then link to it from your HTML using the <script> tag’s src attribute.

Why is this the best way?

  • Separation of Concerns: Your HTML (structure), CSS (styling), and JavaScript (behavior) are in separate files, making your project organized and easier to understand.
  • Reusability: You can use the same .js file across multiple HTML pages.

First, create a file named script.js in the same directory as your HTML file, and put this content inside:

script.js:

console.log("from external script!");

Then, link it in your HTML file:

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>External JS</title>
  </head>
  <body>
    <h1 id="mainHeading">External JavaScript Example</h1>

    <script src="script.js"></script>
  </body>
</html>

When you open index.html, “from external script!” will be in your console.


Variables: The Foundation of Data Storage

Variables are fundamental. They are named containers for storing data values. Imagine them as little labeled boxes in your computer’s memory, where you can put different types of information.

To declare a variable, you use special words in JavaScript. Historically, the main way was using var.

var num = 5;

Let’s break this down:

  • var is a special word in JavaScript called a keyword. You’re essentially telling JS, “Hey, I just mentioned var, so, I want to declare a variable, and whatever I say next will be the name of it. I’ll do you the favor of giving its value as well.”
  • num is the name of the variable. It’s the label on your box. You’ll use this name to refer to the value stored inside.
  • = is the assignment operator. It means, “put the value on the right into the variable on the left.”
  • 5 is the value you are storing in the variable num.
  • ; (semicolon) marks the end of a complete JavaScript statement. While JavaScript can sometimes automatically insert semicolons where it thinks they belong (called “Automatic Semicolon Insertion”), it’s best practice to include them explicitly for clarity and consistency.

So, in your computer’s memory, it now knows that you have a variable named num, and it currently holds the value 5. Neat.

Variables, of course, are designed to hold more than just numbers. They can contain a piece of text, often called a “string”:

var greeting = "some bull"; // Text values go inside quotes
var anotherNumber = 10;

You might wonder what happens if you try naming a variable something like var var = 10; or var function = 5;. JavaScript would throw an error because words like var, function, if, else, and many others are keywords—special words with specific meanings in JavaScript. You cannot use keywords as variable or function names because JavaScript reserves them for language functionality. There are also reserved words that might become keywords in future JavaScript versions, which you should also avoid using as identifiers.

Functions: Reusable Blocks of Code

A function is a reusable block of organized code designed to perform a specific task. Functions are fundamental to writing maintainable, modular code—they allow you to group related statements together and execute them whenever needed.

You declare a function like this:

function sayHello() {
  // This is the code block for the sayHello function
  console.log("Hello from the function!");
}

Again, function is a keyword that tells JavaScript you’re declaring a function. sayHello is the name of the function. The parentheses () usually hold information (called “parameters” or “arguments”) that the function might need to do its job, but for now, we’ll leave them empty; I’ll explain them later. The curly braces {} represent the code block – all the instructions inside these braces belong to this function.

To execute a function, you need to call or invoke it. Declaring a function simply defines what it does—calling it actually executes the code inside:

function sayHello() {
  console.log("hello");
}

sayHello(); // This "calls" the function, executing the code inside it.
sayHello(); // You can call it multiple times!

When you run this, “hello” will be printed to your console twice.


Function Parameters: Giving Functions Information

Functions are much more useful when they can operate on different pieces of information each time they’re called. This is where parameters (also called arguments) come in. Parameters are essentially variables listed inside the function’s parentheses () in the function definition. They act as placeholders for values that you’ll pass into the function when you call it.

When you define a function, you list the parameters it expects:

function greet(name) {
  // 'name' is a parameter
  console.log("hii, " + name + "!");
}

Here, name is a parameter. It’s a variable that will hold the value passed into the greet function.

When you call a function, you provide the actual values (arguments) for those parameters:

greet("Sanchit"); // "Sanchit" is the argument for the 'name' parameter
// Output: hii, Sanchit!

greet("Some Guy"); // "Bob" is another argument
// Output: hii, Some Guy!

Each time greet is called, the name parameter inside the function takes on the value of the argument provided.

You can have multiple parameters, each separated by a comma (,):

function add(num1, num2) {
  // num1 and num2 are parameters
  let sum = num1 + num2;
  console.log("The sum is: " + sum);
}

add(5, 3); // 5 and 3 are arguments for num1 and num2 respectively
// Output: The sum is: 8

add(10, 20);
// Output: The sum is: 30

Optional Parameters and Default Values

What if a parameter isn’t always necessary, or you want it to have a fallback value if not provided? JavaScript allows you to define default parameter values. If an argument is omitted when the function is called, the parameter will take its default value.

function sendMessage(message, sender = "Anonymous") {
  // 'sender' has a default value
  console.log(`${sender} says: "${message}"`);
}

sendMessage("Hello there!"); // 'sender' defaults to "Anonymous"
// Output: Anonymous says: "Hello there!"

sendMessage("Nice to meet you.", "Sanchit"); // 'sender' is provided
// Output: Sanchit says: "Nice to meet you."

sendMessage("I'm fine."); // Still defaults to "Anonymous"
// Output: Anonymous says: "I'm fine."

In this sendMessage function, if the sender argument is not provided when the function is called, it will automatically use "Anonymous". If sender is provided, it will use the provided value. This makes your functions more flexible and easier to use in different scenarios without having to write separate overloaded functions.

Functions can also contain variables:

function calculateNumbers() {
  var num1 = 6;
  var num2 = 9;
  var sum = num1 + num2;
  console.log("The sum is: " + sum); // Output: The sum is: 15
}

calculateNumbers(); // Call the function to run its code

Yeah, we just added two variables/numbers together. Neat, right?

var: The Old, Chill Guy

Now that you understand variables and functions a bit better, let’s circle back to var. As mentioned, var is the original variable declaration keyword. It’s old and generally not used anymore because its behavior can be a bit quirky and lead to confusion, especially with scope and hoisting (which I’ll cover soon).

The main reason var is problematic is its function-scoping behavior. This means if you declare a variable with var inside a function, it’s accessible anywhere within that function, but not outside it. If you declare it outside any function, it becomes a global variable, meaning it’s accessible from anywhere in your script, which can lead to unintentional conflicts.

Consider this problematic behavior:

// Example of var's "quirkiness" with re-declaration
var userStatus = "active";
console.log(userStatus); // Output: active

// Later in your code, maybe inside an 'if' block or a loop...
// You might accidentally re-declare the same variable:
var userStatus = "inactive"; // No error, it just overwrites the previous 'userStatus'
console.log(userStatus); // Output: inactive

// This can make it hard to track where a variable's value truly comes from.

The var keyword is overly permissive—it allows redeclaration and has confusing scoping rules that can lead to subtle bugs in larger applications.

let: The Reasonable Adult

let is the reasonable, modern choice for declaring variables whose values might change. The key difference from var is that let is block-scoped.

A block is any code enclosed in { } curly braces. This includes if statements, for loops, and even just standalone blocks you might create for organization. A variable declared with let is only accessible within the specific block where it was defined. This helps prevent accidental overwrites and makes your code more predictable.

let myAge = 25; // Declared with 'let'
console.log(myAge); // Output: 25

// If you try to re-declare 'myAge' in the same scope, JavaScript will stop you:
// let myAge = 26; // This would cause a SyntaxError: Identifier 'myAge' has already been declared

function myTest() {
  let tempVariable = "only lives inside this for block";
  console.log(tempVariable); // Output: only lives inside this for block

  let myAge = 26; // This is a *new*, separate 'myAge' variable, scoped to this 'function' block
  console.log(myAge); // Output: 26 (This refers to the 'myAge' inside the if block)
}

// Outside the 'function' block:
// console.log(tempVariable); // ReferenceError: tempVariable is not defined (it's block-scoped)
console.log(myAge); // Output: 25 (This refers to the original 'myAge' outside the if block)

This block-scoping behavior is much more intuitive and helps avoid the messy issues that var can introduce.

Consider that same code but with the use of var:

var myAge = 25; // Declared with 'var'
console.log(myAge); // Output: 25

// var is a chill guy, doesn't give AF if you re-declare it
var myAge = 26; // No error this time
console.log(myAge); // Output: 26

function myTest() {
  var tempVariable = "does this live only inside the function block as well?";
  console.log(tempVariable); // Output: does this live only inside the function block as well?

  var myAge = 27;
  console.log(myAge); // Output: 27
}

// Outside the 'function' block:
// console.log(tempVariable); // Again, an error, since var is function scoped.
console.log(myAge); // Output: 26, since var is function scoped, so its value of 27 was only within the myTest function

You can re-assign variables, once they’re assigned. For example, consider this:

var myAge = 18;
console.log(myAge); // Output: 18
myAge = 19;
console.log(myAge); // Output: 19

Notice how we didn’t have to declare the variable again? We just changed its value. Much better, but this is also where troubles begin arising when it comes to the use of var. Again, consider our above example but without re-declaring variables:

var myAge = 25; // Declared with 'var'
console.log(myAge); // Output: 25

myAge = 26;
console.log(myAge); // Output: 26

function myTest() {
  myAge = 27;
  console.log(myAge); // Output: 27
}

console.log(myAge); // Output: 27

Notice how the final console log outputs 27 instead of 26? This happens because we didn’t redeclare myAge inside the function—we simply reassigned the global variable. Since myAge = 27 inside myTest() doesn’t use var, it modifies the existing global variable rather than creating a new function-scoped one.

This behavior demonstrates why var can be problematic in larger applications. Variable assignments can unintentionally modify global state, leading to bugs that are difficult to track down. This is why modern JavaScript development favors let and const, which we’ll explore next.

But before that, here’s another neat piece of info:

We can declare variables first, and just choose to…not assign it any value when declaring it. For example:

let x;

This is a perfectly valid piece of code. We can just assign the value of x later when we need it, like:

let x;

// some piece of code here that would decide whether x needs to have the value of 6 or 9

x = 6; // or x = 9

So, variables declared with let or var don’t need to be initialized with any value at the time of their declaration. It’s perfectly fine to assign their value later.

const: The Strict Parent

const stands for “constant.” Like let, const is block-scoped. The crucial difference is that variables declared with const must be initialized at the time of declaration and cannot be reassigned afterward. They are read-only references.

Default to const for all declarations unless you have a specific reason to use let. This practice prevents many common bugs by making your code’s intent explicit. When you use const, you’re telling JavaScript (and other developers) that this variable will always reference the same value. Only use let when you genuinely need to reassign the variable.

const PI = 3.14159; // PI must be assigned a value immediately
console.log(PI); // Output: 3.14159

// PI = 3.14; // This will cause a TypeError: Assignment to constant variable. (You cannot reassign PI)

const userName = "strict guy";
// userName = "chill guy"; // TypeError: Assignment to constant variable.

This might seem confusing at first, but const ensures the reference to the object remains constant, not the mutability of the object’s contents. I’ll explain them later, probably when we get to arrays/objects, but for now, I explained it just to make you understand that const is not exactly bullet proof like you might expect it to be.


Scope & Hoisting:

Scope and hoisting are fundamental JavaScript concepts that often confuse developers. Understanding them early prevents common mistakes and helps you write more predictable code. Many tutorials postpone these topics, but grasping them now will save you from debugging nightmares later.

Scope: Where Your Variables Live

Scope determines where variables and functions can be accessed in your code. Think of scope like nested containers—a variable declared in an outer container is accessible within all inner containers, but variables in inner containers aren’t accessible from outer ones. Understanding scope is crucial for writing predictable code and avoiding variable naming conflicts.

JavaScript has three main types of scope, as I’ve already discussed:

  1. Global Scope: Variables declared outside any function or block are in the global scope. They are the “loudest” variables because they are accessible from anywhere in your code.

    const globalMessage = "visible everywhere"; // Global scope
    
    function seeGlobal() {
      console.log(globalMessage); // can access 'globalMessage' inside this function
    }
    
    seeGlobal(); // Output: visible everywhere
    console.log(globalMessage); // Output: visible everywhere
    

    While global scope is convenient, overusing global variables creates namespace pollution—when too many variables exist in global scope, different parts of your application can accidentally conflict with each other, causing difficult-to-debug issues.

  2. Function Scope (for var): Variables declared with var inside a function are function-scoped. They are only accessible within that specific function and any functions nested inside it (yes, you can nest functions inside of a function). They are not accessible outside the function.

    function calculateArea(length, width) {
      var area = length * width; // 'area' is function-scoped to calculateArea
      console.log("Calculated area:", area); // Accessible here
    }
    
    calculateArea(10, 5); // Output: Calculated area: 50
    
    // console.log(area); // ReferenceError: area is not defined
    // 'area' is trapped inside the calculateArea function.
    

    This encapsulation is generally good, but var’s function-scoping behavior can still be less intuitive compared to let and const’s block scope, as already explained.

  3. Block Scope (for let and const): Variables declared with let or const inside any block (any code enclosed in { }, like a function block, or even a standalone pair of curly braces) are block-scoped. They are only accessible within that specific block where they were defined.

    let outsideVar = "outside any block";
    
    {
      let insideIfVar = "inside some stand-alone block"; // Block-scoped
      const PI_APPROX = 3.14; // Also block-scoped
    
      console.log(outsideVar); // Output: outside any block (Can access outer scope)
      console.log(insideIfVar); // Output: inside some stand-alone block
      console.log(PI_APPROX); // Output: 3.14
    }
    
    // console.log(insideIfVar);  // ReferenceError: insideIfVar is not defined
    // console.log(PI_APPROX);    // ReferenceError: PI_APPROX is not defined
    // These variables are "imprisoned" within their block.
    

    Block scope is a powerful feature that helps you write cleaner code by limiting the visibility of variables to where they are actually needed, preventing accidental modification from other parts of your program.

Lexical Scope: The “Where You Write It” Rule

Now that we’ve explored global, function, and block scope, let’s tie it all together with a fundamental JS concept: Lexical Scope, AKA Static Scope.

Lexical scope means that the scope of a variable (where it can be accessed) is determined by its position in the source code at the time of compilation/parsing, not at the time the code is executed. In simpler terms, it’s about where you write variables and functions, not where you call them.

Think back to the analogy of nested boxes for scope. Lexical scope dictates that a “child” box (an inner function or block) can always access variables from its “parent” box (the outer function or global scope). However, a “parent” box cannot access variables from its “child” boxes.

Consider this example to solidify the intuition:

const globalVar = "I am global."; // Global scope

function outerFunction() {
  let outerVar = "I am in outerFunction's scope.";

  function innerFunction() {
    let innerVar = "I am in innerFunction's scope.";

    console.log(globalVar); // Accessible: 'innerFunction' can look outwards to the global scope
    console.log(outerVar); // Accessible: 'innerFunction' can look outwards to 'outerFunction's scope
    console.log(innerVar); // Accessible: 'innerFunction' can access its own variables
  }

  innerFunction(); // Call the inner function
  // console.log(innerVar); // ReferenceError: innerVar is not defined.
  // 'outerFunction' cannot look inwards into 'innerFunction's scope.
}

outerFunction(); // Call the outer function
// console.log(outerVar); // ReferenceError: outerVar is not defined.
// Global scope cannot look inwards into 'outerFunction's scope.

In this example:

  • innerFunction is lexically nested inside outerFunction. This means innerFunction has access to its own variables (innerVar), plus any variables declared in outerFunction’s scope (outerVar), and also any variables in the global scope (globalVar).
  • outerFunction can access outerVar and globalVar, but not innerVar because innerVar is declared in a scope nested within outerFunction.
  • The global scope can only access globalVar.

This hierarchical, “outward-looking” nature of scope, determined by the physical placement of your code, is what’s known as lexical scope. It’s important for understanding closures (a topic I’ll cover in the next part, which builds heavily on lexical scope) and for writing clean code.

Hoisting: JavaScript’s Declaration Shuffle

Hoisting is JavaScript’s default behavior of moving variable and function declarations to the top of their current scope before code execution. This doesn’t mean your code physically moves; it’s a conceptual way the JavaScript engine processes declarations during the “compilation” phase (for now, just consider it a phase during the interpretation of JS code, because I’ll dive into it later). Crucially, only the declarations are hoisted, not the initializations (assigning a value).

This can lead to seemingly strange behavior, especially with var.

var and Hoisting: The “Undefined” Surprise

When var declarations are hoisted, they are automatically initialized with the value undefined. This means you can reference a var variable before its actual declaration line in the code without getting an error, but its value will be undefined.

Consider this code:

console.log(myVar); // Output: undefined
var myVar = "declared!";
console.log(myVar); // Output: declared!

What JavaScript conceptually ‘sees’ due to hoisting:

// var myVar;             // Declaration is hoisted to the top of its scope, initialized to undefined
// console.log(myVar);    // So, 'myVar' exists, but its value is undefined
// myVar = "declared!"; // The assignment remains in place
// console.log(myVar);

This behavior can create subtle bugs when variables are accidentally used before receiving their intended values. JavaScript’s hoisting mechanism, while consistent, can lead to unexpected behavior if not properly understood.

let and const and the Temporal Dead Zone (TDZ): The “ReferenceError” Safeguard

let and const declarations are also hoisted. However, unlike var, they are not initialized. Instead, they enter a state called the Temporal Dead Zone (TDZ) from the beginning of their block scope until their actual declaration line is encountered during execution.

Attempting to access a let or const variable within its TDZ results in a ReferenceError. This is actually beneficial—JavaScript forces you to declare and initialize variables before using them, leading to more predictable code and catching potential errors early in development.

// console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization (myLet is in TDZ)
let myLet = "some text";
console.log(myLet); // Output: some text

// console.log(MY_CONST); // ReferenceError: Cannot access 'MY_CONST' before initialization (MY_CONST is in TDZ)
const MY_CONST = 123;
console.log(MY_CONST); // Output: 123

function tdzExample() {
  // 'localVar' is in TDZ from the start of this function scope
  // console.log(localVar); // ReferenceError: Cannot access 'localVar' before initialization
  let localVar = "inside function";
  console.log(localVar); // Output: inside function
}
tdzExample();

The TDZ ensures that you don’t end up using variables that haven’t been properly set up yet, making your code’s flow more explicit and easier to reason about.


Data Types

Understanding data types is fundamental because they dictate what kinds of values you can store in your variables and what operations you can perform on them. If you try to add a string to a number, JavaScript might do something unexpected!

Statically vs. Dynamically Typed: Hammer This Home

This is a critical distinction that separates JavaScript from many other programming languages.

  • Statically Typed Languages (e.g., Java, C++, or even a superset of JS called TypeScript, which we’ll cover soon) are like strict librarians. When you declare a variable, you must tell it what kind of data it will hold (e.g., “this box will only hold numbers,” or “this box will only hold text”). Once declared, that variable can only hold data of that specific type. If you try to put text into a “number” box, the language will yell at you before your program even runs (at “compile-time”). This provides a lot of safety.

  • Dynamically Typed Languages (like JavaScript) are much more casual. You don’t declare the data type of a variable beforehand. The type of a variable is determined at runtime (when the code is actually running) based on the value you assign to it. This offers incredible flexibility: you can assign a number to a variable, and then five lines later, assign a string to it without JS even caring about it.

    let flexibleVar = 10; // JavaScript sees '10' and says, "Okay, 'flexibleVar' is a number now."
    console.log(typeof flexibleVar); // Output: number (typeof tells you the data type)
    
    flexibleVar = "hello"; // Now JavaScript sees a string and says, "Okay, 'flexibleVar' is a string now."
    console.log(typeof flexibleVar); // Output: string
    
    flexibleVar = true; // And now it's a boolean (true/false values).
    console.log(typeof flexibleVar); // Output: boolean
    

    This dynamic nature is a double-edged sword. While it allows for rapid development and flexibility, it also means you lose the early error detection (compile-time checks) that statically typed languages offer. Type-related errors often only surface at runtime in JavaScript, where they can be much harder to detect and fix, leading to unexpected behavior in your applications.

The Primitives: string, number, boolean, null, undefined, symbol, bigint

JS has seven primitive data types. A primitive value is a basic, immutable (cannot be changed after creation) data type. When you assign a primitive value to a variable, you are storing the actual value itself.

  1. string: Represents textual data. Strings are sequences of characters enclosed in single quotes (''), double quotes (""), or backticks (`). Strings are immutable, meaning you can’t change individual characters; operations that appear to change a string actually create a new one.

    let productName = "some product";
    let motto = "shit";
    let dynamicMessage = `text is: ${productName}`; // Backticks allow variable interpolation, meaning you can put the actual value of that variable inside of the string, so dynamicMessage's text becomes: 'text is: some product'
    
  2. number: Represents both integer (whole numbers) and floating-point (numbers with decimals) numbers. JavaScript uses a single number type for both.

    let quantity = 15; // Integer
    let itemPrice = 19.99; // Floating-point
    let total = quantity * itemPrice; // You can perform arithmetic operations
    
  3. boolean: Represents only two values: true or false.

    let userLoggedIn = true;
    let isAdmin = false;
    
  4. null: Represents the intentional absence of any object value. It’s a primitive value used to explicitly indicate “no value.” In JavaScript, typeof null mysteriously returns "object", which is a widely acknowledged bug in the language that has persisted for historical compatibility reasons. It’s essentially a deliberate mistake.

    let activeUser = null; // We explicitly set this to null to mean 'no active user yet'
    
  5. undefined: Represents a variable that has been declared but has not yet been assigned a value. It’s the default value for uninitialized variables. It’s the universe’s way of saying “you messed up” or simply “I don’t have a value for this yet.”

    let favoriteColor; // Declared but not assigned a value
    console.log(favoriteColor); // Output: undefined
    
    // If a function doesn't return anything explicitly, it implicitly returns undefined
    function doNothing() {}
    console.log(doNothing()); // Output: undefined
    
  6. symbol: Represents a unique, immutable value. Each Symbol() call creates a completely new, unique symbol. They are primarily used as unique object property keys to avoid naming collisions, especially in complex codebases or when dealing with third-party libraries. More on these later.

    const uniqueId1 = Symbol("user_id");
    const uniqueId2 = Symbol("user_id");
    console.log(uniqueId1 === uniqueId2); // Output: false (they are unique)
    
  7. bigint: Represents whole numbers larger than 2^53 - 1 (which is the maximum “safe” integer that the standard number type can precisely represent). You create a BigInt by appending n to the end of an integer literal.

    const maxSafeInteger = Number.MAX_SAFE_INTEGER; // 9007199254740991
    const reallyBigNumber = maxSafeInteger + 1; // This might be imprecise with regular 'number'
    console.log(reallyBigNumber); // Output: 9007199254740992 (might not be exact for larger calculations)
    
    const bigIntVersion = 9007199254740991n + 10n; // Use 'n' for BigInt
    console.log(bigIntVersion); // Output: 9007199254741001n (precise)
    

Objects: Everything Else.

If a value is not one of the seven primitive types we just discussed, it’s an Object. Think of objects as the “catch-all” category for more complex data. While primitives are like individual pieces of LEGO, objects are like entire LEGO structures you build – they can hold multiple pieces, organize them, and represent more meaningful, real-world concepts.

Objects are mutable data structures, meaning their contents can be changed after they are created. This is a key difference from primitive types, which are immutable.

The most common type of object you’ll encounter is a plain object. It’s essentially a collection of key-value pairs. Imagine a filing cabinet: each file inside has a label (the key), and inside that file is some information (the value).

You create a plain object using curly braces {}:

let car = {
  // These are key-value pairs (also called properties)
  make: "Honda", // 'make' is the key (a string), "Honda" is the value
  model: "Civic", // 'model' is the key, "Civic" is the value
  year: 2020, // 'year' is the key, 2020 is the value (a number)
  isElectric: false, // 'isElectric' is the key, false is the value (a boolean)
};

In this example, car is an object. It represents a single “car” and holds several pieces of information about that car, each identified by a meaningful name (like make, model, year, isElectric).

To access the values stored inside an object, you use dot notation (objectName.keyName) or bracket notation (objectName[‘keyName’]):

console.log(car.make); // Output: Honda (using dot notation)
console.log(car["year"]); // Output: 2020 (using bracket notation - useful when key names have spaces or are stored in variables)

// You can also change the values of properties within an object:
car.year = 2022; // The object itself is mutable
console.log(car.year); // Output: 2022

car.color = "blue"; // You can even add new properties
console.log(car.color); // Output: blue

Some Important Info About const When it comes to consts, which hold constant values that cannot be changed later, you might assume that the contents of these objects cannot be changed either. That’s wrong. Remember, I said JS just makes sure the reference is the same, constant value. The content inside of the reference can, however, change freely.

For example, consider this:

const car = {
  make: "Honda",
  model: "Civic",
  year: 2020,
};

The car constant references the object and JS make sure that it keeps referencing the object, but the actual content of the object can be changed without making JS throw a fit. So, since we cannot change the main reference, we cannot do something like:

const car = "some trash car";

But it is perfectly fine to directly change the value of a key, like by doing this:

car.year = 2010;
console.log(car.year); // Output: 2010

Like I said before, const isn’t exactly bullet proof.

Beyond plain objects, many other things in JavaScript are also considered objects. For instance:

  • Arrays: These are special types of objects used to store ordered collections of values. Think of them as a list. You define them using square brackets []. We’ll dedicate a whole section to them later.
    let fruits = ["apple", "banana", "cherry"]; // array of strings
    
  • Functions: Yes, even the functions you just learned about are a type of object in JavaScript! This is a powerful concept called “first-class functions,” meaning functions can be treated like any other value – assigned to variables, passed as arguments, and returned from other functions. We’ll explore this much more when we dive deeper into functions.
    function greet(name) {
      // 'greet' is a function (and an object!)
      console.log(`Hello, ${name}!`);
    }
    let myFunctionVariable = greet; // Assigning the function to a variable
    // We can then call it using the variable name:
    myFunctionVariable("World"); // Output: Hello, World!
    

When you assign an object to a variable (like let car = {...} or let fruits = [...]), you are not storing the actual object itself directly into the variable. Instead, you are storing a reference (think of it as a street address) to where that object lives in your computer’s memory. This is a crucial distinction that impacts how objects are copied and manipulated, and we’ll dive much deeper into this important concept when we talk about pass-by-value vs. pass-by-reference in a future section. For now, just remember that objects are the grand, flexible containers for all your complex, non-primitive data.


Sounds good. Here’s the continuation of your article, “SMM (SDE’s Missing Manual): Building the SDE in You: The Only JavaScript Article You’d Ever Need,” with the new sections as planned:


Making Things Actually Do Something

Operators

Operators are special symbols or keywords that perform operations on one or more values (called operands) and produce a result. They are the backbone of any computation or logical decision in your code.

Arithmetic: +, -, *, /, % (modulo), ** (exponentiation)

These operators handle mathematical calculations. While addition (+), subtraction (-), multiplication (*), and division (/) are straightforward, let’s explore the operators that might be less familiar:

  • Modulo (%): This operator returns the remainder of a division operation.

    let remainder1 = 10 % 3;
    console.log(remainder1); // Output: 1
    
    let isEven = 4 % 2;
    console.log(isEven); // Output: 0 (4 is even)
    
  • **Exponentiation (**)**: This operator raises the first operand to the power of the second operand.

    let result1 = 2 ** 3; // 2 to the power of 3 (2 * 2 * 2)
    console.log(result1); // Output: 8
    

Assignment: =, +=, -=, etc.

The assignment operator (=) assigns a value to a variable. Compound assignment operators (+=, -=, *=, /=, %=, **=) are shorthand methods for performing an arithmetic operation and assigning the result back to the same variable.

  • = (Assignment):

    let value = 10; // Assigns the value 10 to 'value'
    
  • += (Add and Assign): x += y is equivalent to x = x + y.

    let score = 100;
    score += 50; // Same as: score = score + 50;
    console.log(score); // Output: 150
    
  • -= (Subtract and Assign): x -= y is equivalent to x = x - y.

    let lives = 3;
    lives -= 1; // Same as: lives = lives - 1;
    console.log(lives); // Output: 2
    

Comparison: == vs. ===

Comparison operators are used to compare two values and return a boolean (true or false) result. This is where JavaScript can get tricky if you’re not careful.

  • Loose Equality Operator (==): This operator compares two values for equality after converting them to a common type (type coercion). This “loose” comparison can lead to unexpected results because JavaScript tries to “help” you by silently converting data types.

    console.log(5 == 5); // Output: true (number to number)
    console.log("5" == 5); // Output: true (string '5' is coerced to number 5)
    console.log(0 == false); // Output: true (0 is coerced to false)
    console.log(null == undefined); // Output: true (special case, they are loosely equal)
    console.log("" == false); // Output: true (empty string is coerced to false)
    console.log([1] == 1); // Output: true (array is coerced to string '1', then to number 1)
    

    As demonstrated, == often returns true even when data types differ. This automatic type conversion, called type coercion, can lead to unexpected results. While JavaScript attempts to be helpful by converting types automatically, this behavior often creates more confusion than convenience.

  • Strict Equality Operator (===): This operator compares two values for equality without performing any type coercion. It checks both the value and the data type. If the types are different, === will always return false. This makes your comparisons predictable and reliable.

    console.log(5 === 5); // Output: true
    console.log("5" === 5); // Output: false
    console.log(0 === false); // Output: false
    console.log("" === false); // Output: false
    console.log([1] === 1); // Output: false
    

    Always prefer === over == unless you have a specific, well-understood reason for type coercion. Strict equality helps prevent subtle type-related bugs and makes your code’s behavior more predictable.

Logical: && (AND), || (OR), ! (NOT)

Logical operators are used to combine or negate boolean (true/false) expressions, producing a boolean result.

  • Logical AND (&&): Returns true if both operands are true. If the first operand is false, it short-circuits and doesn’t evaluate the second.

    let userActive = true;
    let hasPermission = true;
    let canAccess = userActive && hasPermission;
    console.log(canAccess); // Output: true
    
    let isLoggedIn = false;
    let isAdminUser = true;
    let dashboardAccess = isLoggedIn && isAdminUser; // isLoggedIn is false, so no need to check isAdminUser
    console.log(dashboardAccess); // Output: false
    
  • Logical OR (||): Returns true if at least one of the operands is true. If the first operand is true, it short-circuits.

    let hasItemsInCart = true;
    let paymentReceived = false;
    let proceedToCheckout = hasItemsInCart || paymentReceived;
    console.log(proceedToCheckout); // Output: true (because hasItemsInCart is true)
    
    let isWeekend = false;
    let isHoliday = false;
    let canRelax = isWeekend || isHoliday;
    console.log(canRelax); // Output: false
    
  • Logical NOT (!): Reverses the boolean value of its operand. If an expression is true, ! makes it false, and vice-versa.

    let isRaining = true;
    let shouldGoOutside = !isRaining;
    console.log(shouldGoOutside); // Output: false
    
    let isValid = false;
    let isInvalid = !isValid;
    console.log(isInvalid); // Output: true
    

Control Flow: Making Decisions and Repeating Actions

These control structures dictate the flow of your program, allowing it to make decisions and repeat actions.

Conditional Statements: The Foundation of Program Logic

These statements allow you to execute different blocks of code based on whether a condition is true or false.

  • if statement: Executes a block of code if a specified condition is true.

    let userScore = 120;
    if (userScore > 100) {
      console.log("you've reached a high score!");
    }
    // Output: you've reached a high score!
    
  • else if statement: Allows you to test multiple conditions sequentially. If the if condition is false, the program checks the else if condition, and so on.

    let trafficLight = "yellow";
    
    if (trafficLight === "green") {
      console.log("Go!");
    } else if (trafficLight === "yellow") {
      console.log("Prepare to stop.");
    } else if (trafficLight === "red") {
      console.log("Stop!");
    } else {
      console.log("Invalid light color.");
    }
    // Output: Prepare to stop.
    
  • else statement: Executes a block of code if the if condition (or the preceding else if conditions) is false.

    let currentHour = 14; // 2 PM
    if (currentHour < 12) {
      console.log("Good morning!");
    } else {
      console.log("Good afternoon!");
    }
    // Output: Good afternoon!
    

Now that you know if-else, here’s an extra operator

Ternary Operator

The ternary (or conditional) operator is a shorthand for a simple if...else statement. It takes three operands: a condition, an expression to execute if the condition is true, and an expression to execute if the condition is false.

Syntax: condition ? expression-if-true : expression-if-false

let age = 20;
let canVote = age >= 18 ? "Yes, can vote" : "No, too young";
console.log(canVote); // Output: Yes, can vote

let temperature = 10;
let weatherMessage = temperature > 25 ? "It's hot!" : "It's not too hot.";
console.log(weatherMessage); // Output: It's not too hot.

While ternary operators are excellent for simple conditional assignments, nesting multiple ternaries quickly becomes unreadable. Use them for straightforward cases, but prefer if-else statements when logic becomes complex.

Switch Statements: Handling Multiple Conditions

A switch statement is a control flow statement that allows you to execute different blocks of code based on the value of a single expression. It’s often cleaner than a long if-else if-else chain when you are checking a variable against multiple distinct values.

let dayOfWeek = "Wednesday";
let activity;

switch (dayOfWeek) {
  case "Monday":
    activity = "Start the week with meetings.";
    break; // Exits the switch block
  case "Tuesday":
  case "Wednesday": // Multiple cases can share the same code block
    activity = "Mid-week grind: coding and debugging.";
    break;
  case "Thursday":
    activity = "Prepare for weekend planning.";
    break;
  case "Friday":
    activity = "Client demos and wrap-up.";
    break;
  default: // Executes if no case matches
    activity = "Relax or catch up!";
    break;
}
console.log(`Today's activity: ${activity}`); // Output: Today's activity: Mid-week grind: coding and debugging.

The break keyword is crucial. Without it, execution will “fall through” to the next case block, which is rarely the desired behavior and a common source of bugs. The default case is optional but recommended as a fallback.

Loops: Efficient Repetition

Loops allow you to execute a block of code repeatedly until a certain condition is met. They are 10/10 for iterating over collections of data or performing repetitive tasks.

  • for loop: The most common loop. It’s ideal when you know exactly how many times you want the loop to run, or when iterating over a sequence with a defined start and end.

    Syntax: for (initialization; condition; increment/decrement) { // code block }

    • initialization: Executed once before the loop starts (e.g., let i = 0;).
    • condition: Evaluated before each iteration. If true, the loop continues; if false, the loop terminates.
    • increment/decrement: Executed after each iteration (e.g., i++, meaning the value of i + 1).
    for (let i = 0; i < 5; i++) {
      console.log(`Current count: ${i}`);
    }
    // Output:
    // Current count: 0
    // Current count: 1
    // Current count: 2
    // Current count: 3
    // Current count: 4
    
  • while & do...while: Use these when you need to repeat code based on a condition, but be careful to avoid infinite loops.

    • while loop: Executes a block of code as long as a specified condition is true. The condition is evaluated before each iteration. If the condition is initially false, the loop body will never execute.

      let countdown = 3;
      while (countdown > 0) {
        console.log(`${countdown}...`);
        countdown--; // short for countdown = countdown - 1
      }
      console.log("shut down");
      // Output:
      // 3...
      // 2...
      // 1...
      // shut down
      
    • do...while loop: Similar to while, but the loop body is executed at least once, regardless of the condition, because the condition is evaluated after the first iteration.

      let attempt = 0;
      do {
        console.log(`Making attempt ${attempt + 1}`);
        attempt++;
      } while (attempt < 0); // Condition is false, but it runs once
      // Output: Making attempt 1
      

      Exercise caution with while and do...while loops. If your condition never becomes false, you’ll create an infinite loop that can freeze your browser or crash your application. Always ensure your loop condition will eventually become false.

  • for...of (for iterables like arrays) vs. for...in (for object properties): A key distinction that trips up beginners.

    Before we dive into these, let’s briefly understand what an iterable is. In JS, an iterable is an object that can be iterated over (that is, looped over) using for...of. Examples include String, Array, Map, Set, etc. They have a specific Symbol.iterator method that defines how to iterate over their elements.

    • for...of loop: Designed to iterate directly over the values of iterable objects. It gives you the actual data at each step.

      // (Note: I'll explain Arrays in detail later, but this demonstrates the loop's use)
      let colors = ["red", "green", "blue"];
      for (const color of colors) {
        console.log(`Color: ${color}`);
      }
      // Output:
      // Color: red
      // Color: green
      // Color: blue
      
      let myString = "hello";
      for (const char of myString) {
        console.log(`Character: ${char}`);
      }
      // Output:
      // Character: h
      // Character: e
      // Character: l
      // Character: l
      // Character: o
      
    • for...in loop: Designed to iterate over the enumerable property names (keys) of an object. It is not recommended for iterating over arrays because it may iterate over properties in an arbitrary order and can include inherited properties.

      let carDetails = {
        brand: "Toyota",
        model: "Corolla",
        year: 2023,
      };
      
      for (const key in carDetails) {
        console.log(`${key}: ${carDetails[key]}`);
      }
      // Output
      // brand: Toyota
      // model: Corolla
      // year: 2023
      

      The key takeaway: use for...of for arrays and other iterable data structures to get values, and for...in specifically when you need to iterate over object property keys.

Error Handling: Because Your Code Will Break

No matter how carefully you write code, errors will occur. Users provide unexpected input, network requests fail, and external services become unavailable. Professional applications must anticipate these scenarios and handle errors gracefully rather than crashing.

A fundamental principle of software development: never trust external input. Always assume that data from users, APIs, or other systems might be malformed, missing, or malicious. This is where error handling becomes essential.

JavaScript provides the try...catch...finally statement to manage runtime errors (exceptions).

  • try block: This is where you place the code that you suspect might throw an error. If an error occurs within the try block, execution immediately jumps to the catch block. If no error occurs, the catch block is skipped.
  • catch block: This block is executed if an error occurs in the try block. It receives an Error object as an argument, which contains information about the error (e.g., name, message). This is your opportunity to log the error, display a user-friendly message, or attempt to recover.
  • finally block: This block is optional, but if present, its code will always execute, regardless of whether an error occurred in the try block or was caught by the catch block. It’s typically used for cleanup operations, like closing file connections, releasing resources, or performing final logging, irrespective of the try block’s success or failure.

Let’s look at an example. Imagine you’re trying to process user data, and you expect a certain object structure. If it’s not what you expect, trying to access a property might throw an error.

Scenario 1: Code without try...catch

function processUserData() {
  // Attempt to access a property in some userData object that might not exist or might be on a null object
  let userProfile = userData.profile; // Error source if userData is null/undefined
  console.log(`Processing user: ${userProfile}`);
}

If userData here is null or undefined, your script will immediately stop execution and throw a TypeError: Cannot read properties of null (reading 'userData'). Any code after that line will simply not run. In a web application, this could mean a broken user experience; on a server, it could crash the entire process.

Scenario 2: Code with try...catch (and finally)

Now, let’s make it robust using try...catch...finally.

function processUserDataSafely() {
  try {
    // Code that might cause an error
    let userProfile = userData.profile;
    console.log(`Processing user: ${userProfile}`);
  } catch (error) {
    // Code to handle the error
    console.error("An error occurred while processing user data:");
    console.error("Error name:", error.name);
    console.error("Error message:", error.message);
    // You could also notify the user, log to an external service, etc.
  } finally {
    // Code that always runs, regardless of error or success
    console.log("User data processing attempt complete.");
  }
}

In Scenario 2, if userData is null, null.profile throws a TypeError. 3. Execution immediately jumps to the catch block. 4. The catch block logs the error details, allowing you to understand what went wrong without stopping the program. 5. After catch, the finally block executes, performing its cleanup or final reporting. 6. Crucially, the script continues to run normally after the try...catch...finally block.

This demonstrates how try...catch...finally allows your application to gracefully recover from unexpected situations, providing a much better and more stable user experience than just letting the program crash.


Modern JavaScript: Best Practices and Key Concepts

Before diving into DOM manipulation, let’s establish some modern JavaScript practices that will serve you well throughout your development career.

Template Literals: Better String Handling

Template literals (backticks) provide a cleaner way to create strings with embedded expressions:

const name = "Sarah";
const age = 28;

// Old approach
const message = "Hello, my name is " + name + " and I'm " + age + " years old.";

// Modern approach
const modernMessage = `Hello, my name is ${name} and I'm ${age} years old.`;

Template literals also support multi-line strings without concatenation:

const htmlTemplate = `
  <div class="user-card">
    <h2>${name}</h2>
    <p>Age: ${age}</p>
  </div>
`;

Destructuring: Cleaner Variable Assignment

Destructuring allows you to extract values from arrays and objects more elegantly:

// Array destructuring
const colors = ["red", "green", "blue"];
const [primary, secondary, tertiary] = colors;

// Object destructuring
const user = { name: "John", email: "john@example.com", role: "admin" };
const { name, email } = user;

// With default values
const { theme = "light" } = user; // theme will be "light" if not in user object

Arrow Functions: Concise Function Syntax

Arrow functions provide a more compact syntax for simple functions:

// Traditional function
function add(a, b) {
  return a + b;
}

// Arrow function
const add = (a, b) => a + b;

// With single parameter (parentheses optional)
const double = (x) => x * 2;

// With block body
const processData = (data) => {
  const cleaned = data.filter((item) => item != null);
  return cleaned.map((item) => item.toString());
};

Important: Arrow functions behave differently with the this keyword compared to regular functions. We’ll explore this concept in future articles.

The Spread Operator: Flexible Array and Object Manipulation

The spread operator (...) is invaluable for working with arrays and objects:

// Copying arrays
const originalArray = [1, 2, 3];
const copiedArray = [...originalArray]; // [1, 2, 3]

// Combining arrays
const firstHalf = [1, 2, 3];
const secondHalf = [4, 5, 6];
const combined = [...firstHalf, ...secondHalf]; // [1, 2, 3, 4, 5, 6]

// Copying objects
const originalUser = { name: "Alice", age: 30 };
const updatedUser = { ...originalUser, age: 31 }; // { name: "Alice", age: 31 }

Default Parameters: Safer Function Calls

We touched on this earlier, but it’s worth emphasizing how default parameters prevent undefined-related bugs:

function createUser(name = "Anonymous", role = "user", active = true) {
  return { name, role, active };
}

// All these calls work safely
createUser(); // { name: "Anonymous", role: "user", active: true }
createUser("Bob"); // { name: "Bob", role: "user", active: true }
createUser("Admin", "administrator"); // { name: "Admin", role: "administrator", active: true }

Key Takeaway

These modern JavaScript features aren’t just syntactic sugar—they make your code more readable, maintainable, and less prone to bugs. They represent the current standard for professional JavaScript development.


The Document Object Model (DOM)

Early web pages were completely static—you could view content but couldn’t interact with it dynamically. As the web evolved beyond simple document display, we needed a way for JavaScript to “communicate” with HTML and CSS, to inspect elements, modify content, and respond to user interactions.

Enter the Document Object Model (DOM).

At its core, the DOM is a programming interface for web documents. It represents the page so that programs (like your JavaScript) can change the document structure, style, and content. Think of it as a bridge or a structured, object-oriented representation of your HTML document.

When a web page is loaded, the browser creates a DOM of the page. It constructs a tree-like structure where each HTML element, attribute, and piece of text becomes a “node” in that tree.

  • Document: The root of the tree, representing the entire HTML page.
  • Elements: HTML tags like <body>, <h1>, <p>, <img> are represented as element nodes.
  • Attributes: id, class, src, href are represented as attribute nodes.
  • Text: The actual content within elements is represented as text nodes.

Why is this tree structure important? It defines relationships between different parts of the document: parent-child relationships (e.g., <body> is the parent of <h1> and <p>), and sibling relationships (e.g., <h1> and <p> are siblings). JavaScript can then traverse this tree, find specific nodes, and manipulate them.

The DOM essentially turns your static HTML page into a dynamic, interactive playground. It allows JavaScript to:

  • Access HTML elements: Find specific elements on the page.
  • Change HTML content: Modify text, images, or even entire sections of HTML.
  • Change CSS styles: Dynamically alter the appearance of elements.
  • Add and remove HTML elements: Create new elements or delete existing ones from the page.
  • React to user actions: (We’ll cover this in the next section with events, but it’s the ultimate goal of DOM manipulation).

In short, the DOM is the API that makes your web pages come alive. Without it, JS would be useless for creating interactive frontends.

Selecting Elements: Finding Your Way Around the Tree

Before you can manipulate an HTML element with JavaScript, you first need to select or reference it from the DOM. JavaScript provides several methods on the global document object to do this.

Consider this basic HTML structure for our examples:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOM Selection Examples</title>
  </head>
  <body>
    <h1 id="mainHeading">Welcome to the DOM Playground</h1>
    <p class="intro-paragraph">
      This is an introductory paragraph about the DOM.
    </p>
    <p class="intro-paragraph">
      Another paragraph to demonstrate class selection.
    </p>
    <div>
      <img src="placeholder.png" alt="A placeholder image" id="myImage" />
      <button>A simple button</button>
    </div>
  </body>
</html>

document.getElementById()

This is one of the oldest and fastest methods for selecting a single element. It finds an element by its unique id attribute. Since ids are supposed to be unique within an HTML document, this method will always return either a single Element object or null if no element with the specified id is found.

// HTML: <h1 id="mainHeading">welcome</h1>
const headingElement = document.getElementById("mainHeading");
console.log("Selected element by ID:", headingElement);
// Output: Selected element by ID: <h1>...</h1> (the actual HTML element)

const nonExistentElement = document.getElementById("nonExistent");
console.log("Non-existent element by ID:", nonExistentElement);
// Output: Non-existent element by ID: null (no element with that ID)

document.getElementsByClassName()

This method returns an HTMLCollection of all elements in the document that have the specified class name. An HTMLCollection is a live (or “live-updating”) collection, meaning if elements with that class are added or removed from the document after you’ve selected them, the HTMLCollection automatically updates to reflect these changes.

// HTML: <p class="intro-paragraph">...</p>
const introParagraphs = document.getElementsByClassName("intro-paragraph");
console.log("Selected elements by Class Name:", introParagraphs);
// Output: Selected elements by Class Name: HTMLCollection(2) [...]

// You can access elements in the collection using index
console.log("First intro paragraph:", introParagraphs[0]);
// Output: <p class="intro-paragraph">This is an introductory paragraph about the DOM.</p>

// You can iterate over an HTMLCollection
for (let i = 0; i < introParagraphs.length; i++) {
  console.log(`Paragraph ${i + 1}:`, introParagraphs[i]);
}

document.getElementsByTagName()

This method returns an HTMLCollection of all elements with the given tag name (e.g., 'p', 'div', 'img'). Like getElementsByClassName(), it also returns a live HTMLCollection.

// HTML: <p>...</p>, <div>...</div>, <img>...
const allParagraphs = document.getElementsByTagName("p");
console.log("All paragraphs:", allParagraphs);
// Output: All paragraphs: HTMLCollection(2) [...]

const allDivs = document.getElementsByTagName("div");
console.log("All divs:", allDivs);
// Output: All divs: HTMLCollection(1) [...]

document.querySelector()

This is a powerful and versatile method introduced with newer DOM specifications. It returns the first Element within the document that matches the specified CSS selector(s). If no match is found, it returns null.

The power of querySelector lies in its ability to use any valid CSS selector, making your JavaScript selection logic consistent with how you style your elements in CSS.

// Select by ID (like getElementById)
const mainHeading = document.querySelector("#mainHeading");
console.log("Query selected heading (by ID):", mainHeading);
// Output: Query selected heading (by ID): <h1 id="mainHeading">...</h1>

// Select by Class (first one)
const firstIntroParagraph = document.querySelector(".intro-paragraph");
console.log(
  "Query selected first intro paragraph (by Class):",
  firstIntroParagraph
);
// Output: Query selected first intro paragraph (by Class): <p class="intro-paragraph">This is an introductory paragraph...</p>

// Select by Tag Name (first one)
const firstParagraph = document.querySelector("p");
console.log("Query selected first paragraph (by Tag):", firstParagraph);
// Output: Query selected first paragraph (by Tag): <p class="intro-paragraph">This is an introductory paragraph...</p>

// Select using a more complex CSS selector (e.g., div containing an image)
const imageInsideDiv = document.querySelector("div > img");
console.log("Query selected image inside a div:", imageInsideDiv);
// Output: Query selected image inside a div: <img src="placeholder.png"...>

const nonExistentQuery = document.querySelector(".non-existent-class");
console.log("Non-existent query selector:", nonExistentQuery);
// Output: Non-existent query selector: null

document.querySelectorAll()

This method returns a NodeList of all elements in the document that match the specified CSS selector(s). If no matches are found, it returns an empty NodeList.

A crucial distinction: a NodeList returned by querySelectorAll() is static (non-live). This means it’s a snapshot of the DOM at the moment it was queried. If elements matching the selector are added or removed after the NodeList is created, the NodeList will not update. For most purposes, NodeList is array-like and can be easily iterated using for...of or forEach().

// Select all elements with a specific class
const allIntroParagraphs = document.querySelectorAll(".intro-paragraph");
console.log(
  "Query selected all intro paragraphs (by Class):",
  allIntroParagraphs
);
// Output: Query selected all intro paragraphs (by Class): NodeList(2) [...]

// Iterate over the NodeList
for (const p of allIntroParagraphs) {
  console.log("Paragraph content:", p.textContent);
}
// Output:
// Paragraph content: This is an introductory paragraph about the DOM.
// Paragraph content: Another paragraph to demonstrate class selection.

// Select all buttons
const allButtons = document.querySelectorAll("button");
console.log("Query selected all buttons:", allButtons);
// Output: Query selected all buttons: NodeList(1) [...]

// Select elements by attribute presence
const elementsWithId = document.querySelectorAll("[id]");
console.log("All elements with an ID attribute:", elementsWithId);

Which Selector to Use?

Generally, document.querySelector() and document.querySelectorAll() are superior because they offer the flexibility and power of CSS selectors. This allows you to select elements with much more precision and consistency with your styling rules.

  • Use getElementById() if you specifically need to select a single element by its id and know it’s unique. It’s marginally faster for this specific use case.
  • For everything else, lean towards querySelector() (for the first match) and querySelectorAll() (for all matches). They make your selection logic cleaner, more expressive, and more robust across various scenarios.

Manipulating Elements: Changing the Web on the Fly

Once you’ve selected an element, you can modify its content, attributes, and styles using JavaScript.

Changing Text: textContent vs. innerHTML

These properties allow you to get or set the content of an HTML element.

  • element.textContent:

    • Gets or sets the textual content of an element and all its descendants.
    • It treats everything as plain text, meaning any HTML tags within the string you assign to it will be displayed as literal text, not parsed as HTML.
    • Security Benefit: This makes textContent inherently safer against certain attacks, especially when dealing with user-provided input, as it prevents malicious scripts from being injected into your page.
    <p id="myParagraph">Original text content.</p>
    
    const myParagraph = document.getElementById("myParagraph");
    
    // Get text content
    console.log("Original textContent:", myParagraph.textContent); // Output: Original text content.
    
    // Set new text content
    myParagraph.textContent = "New text set by JS.";
    console.log("New textContent:", myParagraph.textContent); // Output: New text set by JS.
    
    myParagraph.textContent = "This is <strong>bold</strong> text.";
    console.log("textContent with HTML tags:", myParagraph.textContent); // Output: This is <strong>bold</strong> text.
    // On the page, you'd literally see the <strong> tags.
    
  • element.innerHTML:

    • Gets or sets the HTML content (including any tags) of an element.
    • When you assign a string containing HTML tags to innerHTML, the browser parses that string as HTML and renders it on the page.
    • Security Risk: Because innerHTML parses the string as HTML, it is vulnerable to certain (XSS) attacks if you assign untrusted (e.g., user-provided) data directly to it. Malicious scripts embedded in the string could be executed. NEVER use innerHTML with untrusted input unless you have thoroughly sanitized it.
    <div id="myDiv">Original div content.</div>
    
    const myDiv = document.getElementById("myDiv");
    
    // Get HTML content
    console.log("Original innerHTML:", myDiv.innerHTML); // Output: Original div content.
    
    // Set new HTML content
    myDiv.innerHTML = "This text is now <em>italic</em> and <u>underlined</u>.";
    console.log("New innerHTML:", myDiv.innerHTML); // Output: This text is now <em>italic</em> and <u>underlined</u>.
    // On the page, "italic" would be italicized and "underlined" would be underlined.
    
    // !!! Potential XSS vulnerability !!!
    // Imagine 'userInput' comes from a form field or API.
    // let maliciousInput = "<img src='x' onerror='alert(\"hackor man!\")'>";
    // myDiv.innerHTML = maliciousInput; // This would execute the alert if uncommented!
    console.log("innerHTML (potential XSS warning!):", myDiv.innerHTML);
    

    Rule of Thumb: Use textContent when you only need to update plain text. Use innerHTML only when you are absolutely sure the content is safe and trusted, or when you are inserting known, controlled HTML structures.

Changing Attributes: setAttribute() and getAttribute()

As you already know, HTML elements have attributes (e.g., src for images, href for links, class, id). You can modify these as well through JS.

  • element.setAttribute(name, value): Sets the value of a specified attribute on an element. If the attribute already exists, its value is updated; otherwise, a new attribute is created.

    <img id="myImage" src="original.png" alt="An original image" />
    <a id="myLink" href="https://oldurl.com">Visit Old Site</a>
    
    const myImage = document.getElementById("myImage");
    const myLink = document.getElementById("myLink");
    
    // Change the 'src' attribute of an image
    myImage.setAttribute("src", "new_image.jpg");
    console.log("Image src changed to:", myImage.getAttribute("src")); // Output: new_image.jpg
    
    // Change the 'alt' attribute
    myImage.setAttribute("alt", "A new descriptive image");
    console.log("Image alt changed to:", myImage.getAttribute("alt")); // Output: A new descriptive image
    
    // Change the 'href' attribute of a link
    myLink.setAttribute("href", "https://newurl.com");
    myLink.setAttribute("target", "_blank"); // Add a new attribute
    console.log("Link href changed to:", myLink.getAttribute("href")); // Output: https://newurl.com
    console.log("Link target attribute:", myLink.getAttribute("target")); // Output: _blank
    
  • element.getAttribute(name): Returns the value of a specified attribute on the element. Returns null if the attribute does not exist.

    console.log("Current image src:", myImage.getAttribute("src")); // Output: new_image.jpg
    console.log(
      "Non-existent attribute:",
      myImage.getAttribute("data-nonexistent")
    ); // Output: null
    

Messing with Styles (element.style)

You can directly modify the inline CSS styles of an element using its style property. This property returns a CSSStyleDeclaration object, which contains all the inline style properties for that element.

Important: When accessing or setting CSS properties via element.style, use camelCase for property names that are typically hyphenated in CSS (e.g., background-color becomes backgroundColor, font-size becomes fontSize).

<p id="styledParagraph">This paragraph will be styled.</p>
const styledParagraph = document.getElementById("styledParagraph");

// Set background color
styledParagraph.style.backgroundColor = "lightblue";
console.log("Background color set to:", styledParagraph.style.backgroundColor); // Output: lightblue

// Set text color
styledParagraph.style.color = "darkblue";

// Set font size
styledParagraph.style.fontSize = "20px";

// Set padding
styledParagraph.style.padding = "15px";

// Multiple properties
styledParagraph.style.border = "2px solid red";

While direct element.style manipulation works, for more complex or theme-based styling, it’s generally recommended to define your styles in a separate CSS stylesheet and then apply or remove CSS classes to elements using element.classList.add(), element.classList.remove(), or element.classList.toggle(). This keeps your styles separated from your JavaScript logic, making your code more maintainable and organized. (We’ll get into classList and its methods when we discuss events and more advanced DOM interactions.)

Creating and Removing Elements: Dynamic HTML Construction

The DOM API also allows you to dynamically build new HTML elements from scratch and inject them into the page, or remove existing ones.

document.createElement(tagName)

This method creates a new Element node with the specified HTML tag name (e.g., 'div', 'p', 'li'). The newly created element is not automatically added to the document. It exists only in JavaScript’s memory until you explicitly append it to an existing element in the DOM.

const newDiv = document.createElement("div");
console.log("Created new div element:", newDiv); // Output: <div></div> (in memory)

const newListItem = document.createElement("li");
console.log("Created new list item element:", newListItem); // Output: <li></li> (in memory)

parentNode.appendChild(childNode)

Once you’ve created an element, or if you have a reference to an existing element, appendChild() allows you to insert it into the document. It appends a node as the last child of a specified parent node.

<ul id="myList">
  <li>Existing List Item 1</li>
</ul>
<div id="containerForNewElements"></div>
const myList = document.getElementById("myList");
const container = document.getElementById("containerForNewElements");

// Create a new list item
const newItem = document.createElement("li");
newItem.textContent = "Dynamically Added List Item";

// Append the new list item to the <ul> element
myList.appendChild(newItem);
console.log("New list item added to UL.");
// The HTML list will now show "Dynamically Added List Item" at the end.

// Create a new paragraph and append it to the container div
const newParagraph = document.createElement("p");
newParagraph.textContent =
  "This paragraph was added dynamically by JavaScript!";
newParagraph.style.color = "purple";
container.appendChild(newParagraph);
console.log("New paragraph added to container div.");

parentNode.removeChild(childNode)

This method removes a specified child node from its parent. To use it, you need a reference to both the parent element and the child element you want to remove.

<div id="parentDiv">
  <p id="childParagraph">This paragraph will be removed.</p>
  <button id="removeButton">Remove Child</button>
</div>
const parentDiv = document.getElementById("parentDiv");
const childParagraph = document.getElementById("childParagraph");

// Before removing
console.log("Child paragraph exists:", parentDiv.contains(childParagraph)); // Output: true

// Remove the child paragraph from its parent div
parentDiv.removeChild(childParagraph);
console.log("Child paragraph removed.");

// After removing
console.log("Child paragraph exists:", parentDiv.contains(childParagraph)); // Output: false (it's gone from the DOM)

// Note: The 'childParagraph' variable still holds a reference to the element in memory,
// but it's no longer part of the document.

Performance Issues to Keep in Mind

Manipulating the DOM is powerful, but it can also be a significant source of performance bottlenecks, especially in complex applications with frequent updates. Browsers have to do a lot of work to render your web page. When you change the DOM, they often have to re-calculate the layout and repaint parts of the screen. These processes are called reflows (or layouts) and repaints. Understanding reflows and repaints is crucial for writing performant DOM manipulation code. For a deeper dive into these concepts, refer to the CSS article in this series.

Key Performance Considerations:

  1. Modify Elements “Offline”: If you need to make several changes to an element or a subtree of elements, it’s more efficient to remove the element from the DOM, make all your modifications, and then re-add it. This triggers only one reflow/repaint instead of many. Even better, you can use document.createDocumentFragment() to build up complex structures in memory before appending them to the live DOM.

    const myUL = document.getElementById("myList");
    // Imagine this list has many items.
    // If you add items one by one, each appendChild can trigger a reflow.
    
    // Better: Use a DocumentFragment
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < 100; i++) {
      const li = document.createElement("li");
      li.textContent = `Item ${i + 1}`;
      fragment.appendChild(li); // Appending to fragment doesn't touch the live DOM
    }
    myUL.appendChild(fragment); // Appending the fragment triggers only one reflow
    
  2. Avoid Accessing Computed Styles in Loops: Requesting certain computed styles (like offsetWidth, offsetHeight, getComputedStyle()) inside a loop can be disastrous for performance. These properties require the browser to perform an immediate reflow to give you the up-to-date value, effectively synchronizing layout calculations which are normally asynchronous and optimized.

    const box = document.getElementById("myBox");
    // Bad (forces reflow on each iteration)
    // for (let i = 0; i < 100; i++) {
    //     box.style.width = (box.offsetWidth + 1) + 'px'; // Reading offsetWidth forces layout
    // }
    
    // Better (calculate once, then apply)
    let currentWidth = box.offsetWidth;
    for (let i = 0; i < 100; i++) {
      currentWidth += 1;
    }
    box.style.width = currentWidth + "px";
    

By understanding how the DOM works and keeping these performance considerations in mind, you can write JavaScript that not only makes your web pages dynamic but also keeps them fast and smooth. For comprehensive DOM documentation, refer to the MDN Web Docs.


Key Takeaways: Building Your JavaScript Foundation

We’ve covered substantial ground in this JavaScript fundamentals guide. Let’s consolidate the essential concepts that will serve you throughout your development career:

Core Language Mastery:

  • Understanding variables (const, let, and why to avoid var)
  • Grasping scope and hoisting behavior
  • Writing functions with proper parameter handling
  • Knowing JavaScript’s data type system and its implications

Modern JavaScript Practices:

  • Using template literals for cleaner string composition
  • Leveraging destructuring for elegant variable assignment
  • Applying arrow functions appropriately
  • Utilizing the spread operator for array and object manipulation

Control Flow and Error Handling:

  • Implementing conditional logic with if-else and switch statements
  • Using loops efficiently while avoiding common pitfalls
  • Building robust applications with proper error handling

DOM Manipulation Foundations:

  • Selecting elements effectively with modern query methods
  • Modifying content, attributes, and styles programmatically
  • Understanding performance implications of DOM operations

What Makes the Difference

The concepts we’ve explored here separate developers who understand JavaScript from those who merely copy and paste code. Understanding scope prevents variable conflicts, proper error handling creates resilient applications, and modern syntax improves code maintainability.

Most importantly, these fundamentals prepare you for advanced JavaScript concepts like closures, prototypes, asynchronous programming, and modern frameworks—topics we’ll explore in future articles.

Building Forward

JavaScript mastery requires both theoretical understanding and practical application. The fundamentals covered here will support every JavaScript framework, library, or advanced concept you encounter.

In upcoming SMM articles, we’ll dive deeper into JavaScript’s more advanced features, explore modern development tools, and build toward full-stack proficiency. Each article builds on this foundation, creating the comprehensive knowledge that defines skilled software engineers.

Take time to experiment with these concepts. Build small projects that combine multiple topics—a simple todo app that uses modern syntax, proper error handling, and DOM manipulation. The intersection of these fundamentals is where true understanding develops.

Ready for the next challenge? Stay tuned for the next part of the SMM series, where we’ll explore advanced JavaScript concepts and modern development patterns.