The Only JavaScript Article You'd Ever Need - 1/7
The Foundation Every Developer Needs
The Reality About JS Learning
Let’s address the elephant in the room. You’ve probably worked through or heard of 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.
JS 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, it’s also notorious for its quirks, historical baggage, and design decisions that can trap inexperienced developers.
This is the missing manual for JS, the comprehensive foundation you need to build robust, scalable software. We’re building the deep understanding that transforms coders into engineers. The difference will become clear as we progress through the series.
Understanding JavaScript’s Role
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, it 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 DOM and respond to user interactions.
Today’s JS landscape is dramatically different. With the introduction of Node.js, JS broke free from browser constraints and became a full-stack language. You can now use it for frontend interfaces, backend servers, mobile applications, desktop software, and even embedded systems. This versatility is why JS knowledge is so valuable.
As we explore JS 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 JS
You’ve got some JavaScript code, cute. But how do you make your web browser actually execute it? There are a few common ways to include it 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 that JS ignores but help developers understand code. Comments are essential for maintaining code and explaining complex logic to your future self and teammates.
3. External JS File (<script src="..."> tag)
This is the most common and recommended way to include JavaScript. You write your JS 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
.jsfile 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
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:
varis a special word in JavaScript called a keyword. You’re essentially telling JS, “Hey, I just mentionedvar, 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.”numis 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.”5is the value you are storing in the variablenum.;(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. They’re 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 versions, which you should also avoid using as identifiers.
Functions
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?
–
The Problem With var
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.
The Solution For var
let
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
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 concepts that often confuse developers. Understanding them early prevents common mistakes and helps you write more predictable code. Many tutorials explain these topics at a way later stage, but I feel like now is a good time to at least make you aware about such concepts.
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.
JS has three main types of scope, as I’ve already discussed:
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 everywhereWhile 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 issues.
Function Scope (for
var): Variables declared withvarinside 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 toletandconst’s block scope, as already explained.Block Scope (for
letandconst): Variables declared withletorconstinside any block (any code enclosed in{ }, like afunctionblock, 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:
innerFunctionis lexically nested insideouterFunction. This meansinnerFunctionhas access to its own variables (innerVar), plus any variables declared inouterFunction’s scope (outerVar), and also any variables in the global scope (globalVar).outerFunctioncan accessouterVarandglobalVar, but not innerVar becauseinnerVaris declared in a scope nested withinouterFunction.- 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
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 in which the 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)
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, JS might do something unexpected.
Statically vs. Dynamically Typed
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: booleanThis 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.
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'number: Represents both integer (whole numbers) and floating-point (numbers with decimals) numbers. JavaScript uses a singlenumbertype for both.let quantity = 15; // Integer let itemPrice = 19.99; // Floating-point let total = quantity * itemPrice; // You can perform arithmetic operationsboolean: Represents only two values:trueorfalse.let userLoggedIn = true; let isAdmin = false;null: Represents the intentional absence of any object value. It’s a primitive value used to explicitly indicate “no value”. In JavaScript,typeof nullmysteriously 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'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: undefinedsymbol: Represents a unique, immutable value. EachSymbol()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)bigint: Represents whole numbers larger than2^53 - 1(which is the maximum “safe” integer that the standardnumbertype can precisely represent). You create aBigIntby appendingnto 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 are also considered objects:
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
[]. I’ll dedicate a whole section to them later.let fruits = ["apple", "banana", "cherry"]; // array of stringsFunctions: 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. I’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.
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 += yis equivalent tox = x + y.let score = 100; score += 50; // Same as: score = score + 50; console.log(score); // Output: 150-=(Subtract and Assign):x -= yis equivalent tox = 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 it 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)==often returnstrueeven 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 returnfalse. 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: falseAlways 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 (
&&): Returnstrueif both operands aretrue. If the first operand isfalse, 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: falseLogical OR (
||): Returnstrueif at least one of the operands istrue. If the first operand istrue, 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: falseLogical NOT (
!): Reverses the boolean value of its operand. If an expression istrue,!makes itfalse, 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
These control structures dictate the flow of your program, allowing it to make decisions and repeat actions.
Conditional Statements
These statements allow you to execute different blocks of code based on whether a condition is true or false.
ifstatement: 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 ifstatement: Allows you to test multiple conditions sequentially. If theifcondition is false, the program checks theelse ifcondition, 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.elsestatement: Executes a block of code if theifcondition (or the precedingelse ifconditions) 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’re aware of if and 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
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.
forloop: 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. Iftrue, the loop continues; iffalse, the loop terminates.increment/decrement: Executed after each iteration (e.g.,i++, meaning the value ofi+ 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: 4while&do...while: Use these when you need to repeat code based on a condition, but be careful to avoid infinite loops.whileloop: Executes a block of code as long as a specified condition is true. The condition is evaluated before each iteration. If the condition is initiallyfalse, 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 downdo...whileloop: Similar towhile, 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 1Exercise caution with
whileanddo...whileloops. 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 includeString,Array,Map,Set, etc. They have a specificSymbol.iteratormethod that defines how to iterate over their elements.for...ofloop: 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: ofor...inloop: 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: 2023The key takeaway: use
for...offor arrays and other iterable data structures to get values, andfor...inspecifically 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.
JS provides the try...catch...finally statement to manage runtime errors (exceptions).
tryblock: This is where you place the code that you suspect might throw an error. If an error occurs within thetryblock, execution immediately jumps to thecatchblock. If no error occurs, thecatchblock is skipped.catchblock: This block is executed if an error occurs in thetryblock. It receives anErrorobject 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.finallyblock: This block is optional, but if present, its code will always execute, regardless of whether an error occurred in thetryblock or was caught by thecatchblock. It’s typically used for cleanup operations, like closing file connections, releasing resources, or performing final logging, irrespective of thetryblock’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.
Take a Break, Re-Read This
As the heading suggests, take a break if you haven’t already. Then re-read this until the foundation is fully built inside of your smooth brain. Because this will help you wherever you go, irrespective of what language you work in.
See you in the next part, learner :)