Complete JavaScript Tutorial (Beginner to Advanced)
Complete JavaScript Tutorial (Beginner to Advanced)
This tutorial covers JavaScript from the very basics to advanced concepts. It's
designed to be comprehensive, but you can jump to sections that are most relevant
to you.
Table of Contents
1. Introduction to JavaScript
2. Basic Concepts
○ Variables
○ Data Types
○ Operators
○ Control Flow
○ Functions
3. Objects and Arrays
○ Objects
○ Arrays
○ Common Array Methods
4. DOM Manipulation
○ Selecting Elements
○ Modifying Elements
○ Event Handling
5. Asynchronous JavaScript
○ Callbacks
○ Promises
○ Async/Await
○ Fetch API
6. Advanced Concepts
○ Closures
○ Prototype and Inheritance
○ Classes
○ Error Handling
○ Modules
○ Iterators and Generators
○ Metaprogramming (Proxy and Reflect)
7. ES6 and Beyond
○ Let and Const
○ Arrow Functions
○ Destructuring
○ Spread and Rest Operators
○ Template Literals
○ Enhanced Object Literals
○ Symbol
○ Set and Map
8. JavaScript in the Browser
○ The Window Object
○ The Document Object
○ Browser Storage (Cookies, Local Storage, Session Storage)
○ Web Workers
9. Best Practices and Further Learning
1. Introduction to JavaScript
JavaScript is a versatile, high-level programming language. It's one of the core
technologies of the web, along with HTML and CSS. Here's what you need to know:
● What is JavaScript? It's a scripting language, meaning it's interpreted (not
compiled). It was initially designed to make web pages interactive.
● What can you do with JavaScript?
○ Front-end web development: Create interactive web pages, handle user
input, and dynamically modify content.
○ Back-end web development: (with Node.js) Build server-side applications,
handle databases, and create APIs.
○ Mobile app development: (with frameworks like React Native) Build apps for
iOS and Android.
○ Desktop app development: (with frameworks like Electron) Build cross-
platform desktop applications.
○ Game development: Create browser-based games.
○ And much more: The possibilities are vast, including machine learning, data
visualization, and more.
● How to run JavaScript:
○ In a browser: Use the browser's developer console (right-click on a page,
select "Inspect" or "Inspect Element", and go to the "Console" tab). You can
also embed JavaScript in an HTML file using the <script> tag.
○ With Node.js: Install Node.js, and you can run JavaScript files from your
command line using node your-file.js.
Here's a very basic HTML page with JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>My First Webpage</title>
</head>
<body>
<h1>Hello, World!</h1>
<button id="myButton">Click Me</button>
<script>
// This is JavaScript code
document.getElementById("myButton").addEventListener("click", function() {
alert("Hello!");
});
</script>
</body>
</html>
Explanation:
● <!DOCTYPE html>: Declares the document type as HTML5.
● <html>, <head>, and <body>: Basic HTML structure.
● <script>: This tag contains the JavaScript code.
● document.getElementById("myButton"): Selects the HTML element with the ID
"myButton".
● .addEventListener("click", function() { ... }): Adds a listener to the button. When
the button is clicked, the function inside {} will run.
● alert("Hello!"): Displays a pop-up box with the message "Hello!".
2. Basic Concepts
Let's dive into the fundamental building blocks of JavaScript.
Variables
Variables are used to store data. You can think of them as containers that hold values.
In modern JavaScript, we declare variables using let or const. var is an older way, but
it's best to avoid it in new code.
● let: Declares a variable that can be reassigned.
● const: Declares a variable that cannot be reassigned (it's constant). However, if a
const variable holds an object or array, you can modify the properties or
elements, but you can't assign a completely new object or array to it.
● var: (Don't use this in modern code) Declares a variable. It has some quirks in
how it handles scope, which can lead to unexpected behavior.
let message = "Hello!"; // Declare a variable named 'message' and assign the string
"Hello!"
console.log(message); // Output: Hello!
// myObject = { name: "Bob", age: 25 }; // Error: Can't reassign the entire object
Key Points:
● Declaration vs. Assignment: let x; is a declaration (you're creating the variable).
x = 10; is an assignment (you're giving the variable a value). You can combine
them: let x = 10;
● Naming: Variable names should be descriptive and follow these rules:
○ Start with a letter, underscore (_), or dollar sign ($).
○ Cannot start with a number.
○ Can contain letters, numbers, underscores, and dollar signs.
○ Are case-sensitive (myVar is different from myvar).
○ Cannot be reserved keywords (like if, for, function).
● camelCase: It's common practice to use camelCase for variable names (e.g.,
myVariableName, numberOfStudents).
Data Types
JavaScript has several built-in data types:
● Primitive Data Types: These are the basic, immutable (cannot be changed
directly) data types.
○ String: Represents text. Enclosed in single quotes ('...'), double quotes ("..."),
or backticks (`...`).
○ Number: Represents numeric values, including integers and floating-point
numbers.
○ BigInt: Represents integers of arbitrary length.
○ Boolean: Represents a logical value: true or false.
○ Undefined: Represents a variable that has been declared but has not yet
been assigned a value.
○ Null: Represents the intentional absence of a value.
○ Symbol: Represents a unique identifier (ES6 feature, we'll cover this later).
● Object (Reference Data Type): A collection of key-value pairs. Objects are
mutable (their properties can be changed).
○ Object: A general container for data.
○ Array: A special type of object for storing ordered lists of values.
○ Function: Technically, functions are also objects in JavaScript.
// String
let name = "John Doe";
let greeting = `Hello, ${name}!`; // Template literal (ES6)
// Number
let age = 30;
let price = 19.99;
// BigInt
const largeNumber = 12345678901234567890123456789012345n;
// Boolean
let isAdult = true;
let isRaining = false;
// Undefined
let city; // city is declared, but not assigned a value
console.log(city); // Output: undefined
// Null
let emptyValue = null; // Represents the intentional absence of a value
// Object
let person = {
name: "Alice",
age: 25,
city: "New York"
};
// Array
let numbers = [1, 2, 3, 4, 5];
Key Points:
● Template Literals: Backticks (`...`) allow you to embed variables directly within
strings using ${variableName}.
● Typeof Operator: You can use the typeof operator to determine the data type of
a value or variable:
console.log(typeof name); // Output: "string"
console.log(typeof age); // Output: "number"
console.log(typeof isAdult); // Output: "boolean"
console.log(typeof city); // Output: "undefined"
console.log(typeof emptyValue); // Output: "object" (Note: This is a quirk of
JavaScript)
console.log(typeof person); // Output: "object"
console.log(typeof numbers); // Output: "object"
console.log(typeof greet); // Output: "function"
console.log(typeof largeNumber); //Output: "bigint"
Operators
Operators are symbols that perform operations on values (called operands). Here's a
breakdown:
● Arithmetic Operators: Perform mathematical calculations.
○ +: Addition
○ -: Subtraction
○ *: Multiplication
○ /: Division
○ %: Modulo (remainder after division)
○ **: Exponentiation (ES7)
○ ++: Increment (add 1)
○ --: Decrement (subtract 1)
● Assignment Operators: Assign values to variables.
○ =: Simple assignment
○ +=: Add and assign (e.g., x += 5 is the same as x = x + 5)
○ -=: Subtract and assign
○ *=: Multiply and assign
○ /=: Divide and assign
○ %=: Modulo and assign
○ **=: Exponentiation and assign
● Comparison Operators: Compare values and return a Boolean result (true or
false).
○ ==: Equal to (values are compared after type coercion)
○ ===: Strict equal to (values and types must be the same)
○ !=: Not equal to (values are compared after type coercion)
○ !==: Strict not equal to (values and types must be different)
○ >: Greater than
○ <: Less than
○ >=: Greater than or equal to
○ <=: Less than or equal to
● Logical Operators: Combine or modify Boolean values.
○ &&: Logical AND (returns true if both operands are true)
○ ||: Logical OR (returns true if at least one operand is true)
○ !: Logical NOT (reverses the Boolean value)
● String Operators:
○ +: Concatenation (joins strings together)
● Other Operators:
○ ?:: Conditional (ternary) operator
○ ,: Comma operator
○ typeof: Returns the data type of a value
○ instanceof: Checks if an object is an instance of a constructor
○ in: Checks if a property exists in an object
// Arithmetic Operators
let x = 10;
let y = 5;
// Assignment Operators
let a = 20;
a += 5; // a = a + 5
console.log(a); // Output: 25
// Comparison Operators
console.log(x == "11"); // Output: true (type coercion: "11" is converted to 11)
console.log(x === "11"); // Output: false (strict comparison: types are different)
console.log(x != y); // Output: true
console.log(x !== y); // Output: true
console.log(x > y); // Output: true
console.log(x < y); // Output: false
console.log(x >= y); // Output: true
console.log(x <= y); // Output: false
// Logical Operators
let p = true;
let q = false;
// String Operator
let firstName = "John";
let lastName = "Doe";
let fullName = firstName + " " + lastName;
console.log(fullName); // Output: John Doe
// typeof Operator
console.log(typeof x); //Output: "number"
// in operator
const myCar = {make: 'Honda', model: 'Accord', year: 1998};
console.log('make' in myCar); // Output: true
console.log('color' in myCar); // Output: false
Key Points:
● Loose vs. Strict Equality: == compares values after converting them to a
common type (type coercion). === compares values without type coercion. It's
almost always better to use === to avoid unexpected behavior. The same applies
to != and !==.
● Operator Precedence: Operators have a specific order of precedence (like in
math). For example, multiplication and division are performed before addition and
subtraction. Use parentheses () to control the order of operations if needed.
Control Flow
Control flow statements determine the order in which code is executed. They allow
you to make decisions and repeat actions.
● Conditional Statements: Execute different blocks of code based on conditions.
○ if statement: Executes a block of code if a condition is true.
○ if...else statement: Executes one block of code if a condition is true, and
another block if it's false.
○ if...else if...else statement: Chains multiple conditions together.
○ switch statement: Selects one of several code blocks to execute based on
the value of an expression.
● Loop Statements: Repeat a block of code multiple times.
○ for loop: Executes a block of code a specific number of times.
○ while loop: Executes a block of code as long as a condition is true.
○ do...while loop: Similar to while, but the code block is executed at least once.
○ for...in loop: Iterates over the properties of an object.
○ for...of loop: Iterates over iterable objects (like arrays, strings, maps, sets)
(ES6).
● Jump Statements:
○ break: Terminates a loop or switch statement.
○ continue: Skips the rest of the current iteration of a loop and continues with
the next iteration.
// Conditional Statements
let age = 20;
// Loop Statements
for (let i = 0; i < 5; i++) {
console.log("Iteration:", i);
}
let count = 0;
while (count < 3) {
console.log("Count:", count);
count++;
}
let j = 0;
do {
console.log("Do While:", j);
j++;
} while (j < 3);
// Jump Statements
for (let i = 0; i < 10; i++) {
if (i === 3) {
break; // Exit the loop when i is 3
}
console.log("Break:", i);
}
Key Points:
● Truthy and Falsy: In JavaScript, values are not strictly true or false. Some values
are considered "truthy" (evaluate to true in a Boolean context), and others are
"falsy" (evaluate to false).
○ Falsy values: false, 0, "" (empty string), null, undefined, NaN (Not a Number).
○ Truthy values: All values that are not falsy (e.g., true, 1, "hello", [ ] (empty
array), { } (empty object)).
● Short-circuiting: Logical operators && and || can exhibit short-circuiting
behavior.
○ &&: If the first operand is false, the second operand is not evaluated, and the
result is false.
○ ||: If the first operand is true, the second operand is not evaluated, and the
result is true.
Functions
Functions are reusable blocks of code that perform a specific task. They are essential
for organizing your code and making it more modular.
● Function Declaration: Defines a function with a name.
● Function Expression: Defines a function as part of an expression (often
assigned to a variable).
● Parameters and Arguments: Functions can accept input values called
parameters. When you call a function, you provide the actual values, which are
called arguments.
● Return Value: Functions can optionally return a value using the return statement.
If a function doesn't have a return statement, or if the return statement doesn't
specify a value, the function returns undefined.
● Scope: The scope of a variable determines where it can be accessed. Variables
declared with let and const have block scope (they are only accessible within the
code block where they're defined). Variables declared with var have function
scope (they are accessible within the entire function).
// Function Declaration
function greet(name) {
return "Hello, " + name + "!";
}
// Function Expression
const add = function(a, b) {
return a + b;
};
function sayHello(name) {
console.log("Hello, " + name);
// No return statement, so the function implicitly returns undefined
}
// Scope
function myFunction() {
let localVar = "This is local"; // local variable
var varVar = "var variable";
const constVar = "const variable";
console.log(localVar);
console.log(varVar);
console.log(constVar);
}
myFunction();
// console.log(localVar); // Error: localVar is not defined outside myFunction
console.log(varVar); // Accessible, because var has function scope.
// console.log(constVar); // Error: constVar is not defined outside myFunction.
if (true) {
let blockVar = "Block variable";
var varVar2 = "var variable 2";
console.log(blockVar);
}
// console.log(blockVar); // Error: blockVar is not defined outside the if block
console.log(varVar2); // Accessible, because var has function scope
Key Points:
● Hoisting: Function declarations are hoisted, which means you can call the
function before it's actually defined in the code. Function expressions are not
hoisted (you'll get an error if you try to call them before they're defined). var
declarations are hoisted, but their assignments are not. let and const
declarations are also hoisted, but you cannot access them before their
declaration in the code, which will lead to a ReferenceError.
● First-Class Functions: In JavaScript, functions are first-class citizens. This
means you can:
○ Assign functions to variables.
○ Pass functions as arguments to other functions.
○ Return functions from other functions.
○ Store functions in data structures (like objects and arrays).
● Anonymous Functions: Functions without a name are called anonymous
functions. Function expressions often use anonymous functions.
● Immediately Invoked Function Expressions (IIFEs): A function expression that
is executed immediately after it's defined. Used to create a new scope and avoid
polluting the global scope.
(function() {
let message = "Hello from IIFE!";
console.log(message);
})(); // The () at the end immediately invokes the function
An object is a collection of key-value pairs. The keys are typically strings (but can also
be symbols), and the values can be any data type (including other objects, arrays,
and functions). Objects are used to represent entities with properties and behaviors.
● Creating Objects:
○ Object literal: Using curly braces {}.
○ Constructor function: Using the new keyword with a constructor function
(older way, but still used).
○ Object.create(): Creates a new object with the specified prototype object
and properties.
○ Class (ES6): A more modern way to create objects (syntactic sugar over
constructor functions and prototypes).
● Accessing Properties:
○ Dot notation: objectName.propertyName
○ Bracket notation: objectName["propertyName"] (useful when property
names have spaces or special characters, or when the property name is a
variable).
● Modifying Properties:
○ Assign a new value to an existing property: objectName.propertyName =
newValue;
○ Add a new property: objectName.newPropertyName = value;
○ Delete a property: delete objectName.propertyName;
● Methods: Functions that are associated with an object are called methods. They
are defined as properties whose values are functions. Inside a method, you can
use the this keyword to refer to the object itself.
// Object literal
const person = {
name: "Alice",
age: 30,
city: "New York",
greet: function() {
console.log("Hello, my name is " + this.name);
}
};
// Object.create()
const animal = {
type: 'mammal',
describe() {
console.log(`This is a ${this.type}`);
}
};
Key Points:
● this Keyword: Inside a method, this refers to the object that the method is called
on. The value of this depends on how the function is called.
● Object.keys(), Object.values(), Object.entries(): These methods provide ways
to get arrays of the keys, values, or key-value pairs of an object, respectively.
const myObject = { a: 1, b: 2, c: 3 };
console.log(Object.keys(myObject)); // Output: ["a", "b", "c"]
console.log(Object.values(myObject)); // Output: [1, 2, 3]
console.log(Object.entries(myObject)); // Output: [["a", 1], ["b", 2], ["c", 3]]
● Computed Property Names (ES6): You can use expressions within square
brackets when defining object properties.
const prefix = "user";
const user = {
[`${prefix}Name`]: "John", // Computed property name
[`${prefix}Age`]: 30
};
console.log(user); // Output: { userName: "John", userAge: 30 }
Arrays
An array is an ordered list of values. The values in an array can be of any data type,
and you can even have arrays of arrays (multidimensional arrays). Arrays are a special
type of object in JavaScript, optimized for storing sequences of data.
● Creating Arrays:
○ Array literal: Using square brackets [].
○ new Array() constructor: (Less common, generally avoid)
● Accessing Elements:
○ Array elements are accessed using their index, which starts at 0. So, the first
element is at index 0, the second element is at index 1, and so on.
○ Use square brackets to access elements: arrayName[index]
● Modifying Elements:
○ Assign a new value to an existing element: arrayName[index] = newValue;
○ Add elements:
■ push(): Adds one or more elements to the end of the array.
■ unshift(): Adds one or more elements to the beginning of the array.
■ splice(): Adds or removes elements from any position in the array.
○ Remove elements:
■ pop(): Removes the last element from the array.
■ shift(): Removes the first element from the array.
■ splice(): Removes elements from any position in the array.
○ length property: The length property of an array returns the number of
elements in the array.
// Array literal
const fruits = ["apple", "banana", "orange"];
console.log(fruits.length); // Output: 3
Key Points:
● Arrays are zero-indexed.
● Arrays can hold values of different data types (e.g., [1, "hello", true, { name:
"John" }]).
● The length property is dynamic; it's automatically updated when you add or
remove elements.
● You can create multidimensional arrays (arrays of arrays):
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
console.log(matrix[0][0]); // Output: 1
console.log(matrix[1][2]); // Output: 6
// forEach()
numbers.forEach(function(number) {
console.log(number * 2);
}); // Output: 2, 4, 6, 8, 10
// map()
const doubledNumbers = numbers.map(function(number) {
return number * 2;
});
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Original array is unchanged
// filter()
const evenNumbers = numbers.filter(function(number) {
return number % 2 === 0;
});
console.log(evenNumbers); // Output: [2, 4]
console.log(numbers); // Original array is unchanged
// reduce()
const sum = numbers.reduce(function(accumulator, currentValue) {
return accumulator + currentValue;
}, 0); // 0 is the initial value of the accumulator
console.log(sum); // Output: 15
console.log(numbers); // Original array is unchanged
// find()
const firstEven = numbers.find(number => number % 2 === 0);
console.log(firstEven); // Output: 2
// findIndex()
const firstEvenIndex = numbers.findIndex(number => number % 2 === 0);
console.log(firstEvenIndex); // Output: 1
// sort()
const unsorted = [3, 1, 4, 1, 5, 9, 2, 6];
const sorted = unsorted.sort(); // Sorts alphabetically, which is not correct for
numbers
console.log(sorted); // Output: [1, 1, 2, 3, 4, 5, 6, 9]
// concat()
const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const combinedArray = array1.concat(array2);
console.log(combinedArray); // Output: [1, 2, 3, 4, 5, 6]
// slice()
const slicedArray = numbers.slice(1, 4); // Returns elements from index 1 up to (but
not including) index 4
console.log(slicedArray); // Output: [2, 3, 4]
console.log(numbers); // Original array is unchanged
// splice() - We saw this earlier for adding/removing. It modifies the original array.
const months = ["Jan", "Feb", "Mar", "Apr", "May"];
months.splice(2, 1, "March"); // Replace 1 element at index 2 with "March"
console.log(months); // Output: ["Jan", "Feb", "March", "Apr", "May"]
// join()
const joinedString = numbers.join(", ");
console.log(joinedString); // Output: "1, 2, 3, 4, 5"
// reverse()
const reversedNumbers = numbers.reverse(); // Modifies the original array
console.log(reversedNumbers); // Output: [5, 4, 3, 2, 1]
console.log(numbers); // Original array is now reversed
// includes()
console.log(numbers.includes(3)); // Output: true
console.log(numbers.includes(10)); // Output: false
// indexOf()
console.log(numbers.indexOf(3)); // Output: 2
console.log(numbers.indexOf(10)); // Output: -1
// lastIndexOf()
const arr = [2, 5, 9, 5];
console.log(arr.lastIndexOf(5)); // Output: 3
console.log(arr.lastIndexOf(7)); // Output: -1
// isArray()
console.log(Array.isArray(numbers)); // Output: true
console.log(Array.isArray({})); // Output: false
Key Points:
● Immutable vs. Mutable Methods: It's important to distinguish between array
methods that mutate (modify) the original array and those that return a new array
without changing the original. For example:
○ Mutating: push(), pop(), shift(), unshift(), splice(), sort(), reverse()
○ Non-mutating: map(), filter(), reduce(), concat(), slice(), join()
● Chaining: Many array methods return a new array, which allows you to chain
them together to perform multiple operations in a concise way.
const result = numbers
.filter(num => num % 2 !== 0) // Filter out even numbers: [1, 3, 5]
.map(num => num * 2) // Double each odd number: [2, 6, 10]
.reduce((acc, curr) => acc + curr, 0); // Sum the doubled odd numbers: 18
console.log(result); // Output: 18
● Iterating over arrays: Besides forEach, for...of loop is the modern way to iterate
over arrays:
for (const number of numbers) {
console.log(number);
}
4. DOM Manipulation
The Document Object Model (DOM) is a programming interface for HTML documents.
It represents the structure of the document as a tree of objects, where each object
represents a part of the HTML document (e.g., an element, an attribute, or a text
node). JavaScript can use the DOM to access and manipulate HTML elements,
allowing you to dynamically change the content, structure, and style of a web page.
● The DOM Tree: The DOM represents the HTML document as a tree-like
structure.
○ The document object is the root of the DOM tree.
○ HTML elements are represented as objects called nodes.
○ Nodes have relationships with each other (parent, child, sibling).
● Selecting Elements: To manipulate an HTML element, you first need to select it.
The document object provides several methods for selecting elements:
○ document.getElementById(id): Selects the element with the specified ID. IDs
should be unique within a document.
○ document.getElementsByClassName(className): Selects all elements with
the specified class name. Returns an HTMLCollection (an array-like object).
○ document.getElementsByTagName(tagName): Selects all elements with the
specified tag name (e.g., "p", "div", "a"). Returns an HTMLCollection.
○ document.querySelector(selector): Selects the first element that matches the
specified CSS selector (e.g., "#myId", ".myClass", "p", "div > span").
○ document.querySelectorAll(selector): Selects all elements that match the
specified CSS selector. Returns a NodeList (an array-like object).
● Modifying Elements: Once you've selected an element, you can modify its
properties and content:
○ innerHTML: Gets or sets the HTML content of an element (can include HTML
tags).
○ textContent: Gets or sets the text content of an element (ignores HTML tags).
Use this when you are setting plain text to avoid security vulnerabilities.
○ style: Gets or sets the inline CSS styles of an element.
○ setAttribute(name, value): Sets the value of an attribute.
○ getAttribute(name): Gets the value of an attribute.
○ classList: Provides methods for working with the CSS classes of an element:
■ classList.add(className): Adds one or more classes.
■ classList.remove(className): Removes one or more classes.
■ classList.toggle(className): Toggles a class (adds it if it's not present,
removes it if it is).
■ classList.contains(className): Checks if the element has a specific class.
○ Creating new elements:
■ document.createElement(tagName): Creates a new HTML element.
■ document.createTextNode(text): Creates a new text node.
○ Adding and removing elements:
■ parentElement.appendChild(newElement): Appends a new element as the
last child of a parent element.
■ parentElement.insertBefore(newElement, referenceElement): Inserts a
new element before a reference element.
■ element.remove(): Removes the element from the DOM.
■ parentElement.removeChild(childElement): Removes a child element from
a parent element.
● Event Handling: Events are actions or occurrences that happen in the browser
(e.g., a user clicks a button, a page finishes loading, a form is submitted). Event
handling is the process of responding to these events.
○ addEventListener(eventType, callbackFunction): Attaches an event listener to
an element. When the specified event occurs on that element, the callback
function is executed.
○ Common event types:
■ click: A mouse click.
■ mouseover: The mouse pointer is moved onto an element.
■ mouseout: The mouse pointer is moved away from an element.
■ keydown: A key is pressed down.
■ keyup: A key is released.
■ submit: A form is submitted.
■ load: A page or an element (like an image) has finished loading.
■ change: The value of an input element has changed.
○ Event object: When an event occurs, an event object is created. This object
contains information about the event (e.g., the target element, the
coordinates of a mouse click, the key that was pressed). The event object is
passed as an argument to the callback function.
○ removeEventListener(eventType, callbackFunction): Detaches an event
listener from an element. It's important to remove listeners when you no
longer need them to prevent memory leaks.
○ preventDefault(): Prevents the default behavior of an event (e.g., preventing a
form from submitting and reloading the page, preventing a link from
navigating to a new page).
○ stopPropagation(): Stops the event from propagating (bubbling up) to parent
elements.
○ Event bubbling: Events propagate up the DOM tree from the target element
to its parent elements. This is called event bubbling.
○ Event capturing: Events propagate down the DOM tree. This is less common
than bubbling, and you need to specify it when adding the event listener:
element.addEventListener(eventType, callbackFunction, { capture: true })
<!DOCTYPE html>
<html>
<head>
<title>DOM Manipulation Example</title>
<style>
.highlight {
background-color: yellow;
font-weight: bold;
}
#myDiv {
width: 200px;
height: 100px;
background-color: lightblue;
border: 1px solid black;
padding: 10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1 id="mainHeading">Welcome!</h1>
<p class="myParagraph">This is a paragraph.</p>
<p class="myParagraph">Another paragraph.</p>
<div id="myDiv">This is a div.</div>
<button id="myButton">Click Me</button>
<a href="https://www.example.com" id="myLink">Visit Example.com</a>
<script>
// Selecting Elements
const heading = document.getElementById("mainHeading");
console.log(heading);
// Modifying Elements
heading.innerHTML = "Welcome to My Page!"; // Change the heading text
heading.style.color = "blue"; // Change the heading color
// Removing Elements
// document.body.removeChild(newParagraph); // Remove the new paragraph
// newParagraph.remove();
// Event Handling
myButton.addEventListener("click", function(event) {
alert("Button clicked!");
console.log(event); // Log the event object
console.log(event.target); // Log the target element (the button)
heading.classList.toggle("highlight"); // Toggle class on heading
});
myLink.addEventListener("click", function(event) {
event.preventDefault(); // Prevent the link from navigating
console.log("Link clicked, but navigation prevented!");
});
paragraphs[0].addEventListener("mouseover", function() {
this.style.backgroundColor = "yellow"; // this refers to the paragraph
});
paragraphs[0].addEventListener("mouseout", function() {
this.style.backgroundColor = "";
});
document.body.addEventListener('click', () => {
console.log('Body clicked');
});
Key Points:
● NodeList vs. HTMLCollection: Both are array-like objects, but they have some
differences. NodeList can contain any type of node (elements, text nodes, etc.),
while HTMLCollection only contains element nodes. Also, NodeList returned by
querySelectorAll is static (it doesn't update automatically if the DOM changes),
while HTMLCollection is live (it automatically updates when the DOM changes).
It's generally better to use querySelectorAll because it's more flexible and
consistent.
● Event Delegation: Instead of attaching event listeners to many individual
elements, you can attach a single listener to a parent element and use the
event.target property to determine which element triggered the event. This can
improve performance, especially when dealing with a large number of elements.
● DocumentFragment: If you need to make multiple changes to the DOM, it's
more efficient to use a DocumentFragment. A DocumentFragment is a
lightweight, virtual DOM node. You can add elements to the DocumentFragment,
and then append the entire DocumentFragment to the DOM. This reduces the
number of reflows and repaints, which can improve performance.
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const p = document.createElement("p");
p.textContent = "Paragraph " + (i + 1);
fragment.appendChild(p);
}
document.body.appendChild(fragment); // Only one reflow and repaint
5. Asynchronous JavaScript
JavaScript is single-threaded, which means it can only execute one operation at a
time. This can lead to problems when performing long-running operations, such as
fetching data from a server or reading a large file. If you perform these operations
synchronously (in the main thread), the browser will become unresponsive until the
operation is complete. To avoid this, JavaScript provides mechanisms for performing
asynchronous operations.
● Synchronous vs. Asynchronous:
○ Synchronous: Operations are executed one after another, in a blocking
manner. Each operation must complete before the next one can start.
○ Asynchronous: Operations can start and run independently of the main
thread. The program can continue to execute other code while the
asynchronous operation is in progress. When the asynchronous operation is
complete, a callback function is typically used to handle the result.
● The Event Loop: JavaScript uses an event loop to manage asynchronous
operations. The event loop continuously checks the message queue for tasks to
execute.
○ Call Stack: The call stack is where synchronous function calls are managed.
When a function is called, it's pushed onto the stack. When the function
returns, it's popped off the stack.
○ Message Queue (Task Queue): Asynchronous operations (like setTimeout,
fetch, and event handlers) don't go directly onto the call stack. Instead, they
are placed in the message queue. When the call stack is empty, the event
loop takes the first task from the message queue and pushes it onto the call
stack to be executed.
○ Web APIs: The browser provides Web APIs (like setTimeout, fetch, and DOM
event listeners) that handle the actual asynchronous operations. These APIs
run in the background, outside of the main JavaScript thread.
● Callbacks: A callback function is a function that is passed as an argument to
another function and is executed after the asynchronous operation is complete.
function doSomething(callback) {
setTimeout(function() {
console.log("Async operation complete");
callback("Result"); // Call the callback function with the result
}, 1000); // Simulate a 1-second delay
}
function handleResult(result) {
console.log("Result:", result);
}
// Promise.all()
const promise1 = Promise.resolve(1);
const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
const promise3 = new Promise(resolve => setTimeout(() => resolve(3), 500));
// Promise.race()
const racePromise1 = new Promise(resolve => setTimeout(() => resolve('one'),
500));
const racePromise2 = new Promise(resolve => setTimeout(() => resolve('two'),
100));
Promise.race([racePromise1, racePromise2])
.then(value => console.log(value)); // Output: "two"
● Async/Await: async and await are syntactic sugar over Promises, introduced in
ES2017. They provide a more concise and readable way to work with
asynchronous code, making it look more like synchronous code.
○ async function: A function declared with the async keyword. Async functions
always return a Promise. If you return a value from an async function, it will be
wrapped in a resolved Promise. If you throw an error, it will be wrapped in a
rejected Promise.
○ await operator: The await operator can only be used inside an async function.
It pauses the execution of the function until the Promise that is being awaited
is settled (either fulfilled or rejected). If the Promise is fulfilled, await returns
the fulfilled value. If the Promise is rejected, await throws an error (which you
can catch using a try...catch block).
async function fetchDataAsync() {
try {
console.log("Fetching data...");
const result = await new Promise(resolve => setTimeout(() => resolve({ name:
"Jane", age: 25 }), 1000));
console.log("Data:", result);
const processedResult = "Processed " + result.name;
console.log("Processed result:", processedResult);
return processedResult;
} catch (error) {
console.error("Error:", error);
throw error; // Re-throw the error to be caught by the caller, if needed
}
}
fetchDataAsync()
.then(finalResult => console.log("Final Result", finalResult))
.catch(error => console.log("Caught outside", error));
● Fetch API: The Fetch API provides a modern, Promise-based interface for
making HTTP requests (e.g., getting data from a server). It's a replacement for
the older XMLHttpRequest object.
○ fetch(url, options): The fetch() function takes a URL as its first argument and
an optional options object as its second argument. It returns a Promise that
resolves to the Response object representing the server's response.
○ Response object: The Response object provides methods for accessing the
response headers, status code, and body. To get the actual data from the
response body, you need to use one of the following methods:
■ response.text(): Returns a Promise that resolves to the response body as
a string.
■ response.json(): Returns a Promise that resolves to the response body
parsed as JSON.
■ response.blob(): Returns a Promise that resolves to the response body as
a Blob (a raw binary data object).
■ response.arrayBuffer(): Returns a Promise that resolves to the response
body as an ArrayBuffer.
○ Request headers: You can set request headers using the headers option in
the fetch() function.
○ HTTP methods: The fetch() function defaults to the GET method. To use other
HTTP methods (e.g., POST, PUT, DELETE), you need to specify the method
option.
○ Request body: To send data in the request body (e.g., with POST or PUT
requests), you need to use the body option. The data should be a string. If
you're sending JSON data, you need to stringify it using JSON.stringify().
// GET request
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json(); // Parse the response body as JSON
})
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
// POST request
fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8"
},
body: JSON.stringify({
title: "foo",
body: "bar",
userId: 1
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
getPosts();
Key Points:
● Error Handling: It's crucial to handle errors in asynchronous operations. With
callbacks, you typically use an error-first callback pattern (the first argument to
the callback is an error object). With Promises, you use .catch(). With
async/await, you use try...catch. The Fetch API's fetch() function only rejects for
network errors, not for HTTP errors (like 404 or 500). You need to check the
response.ok property in the then() block to handle HTTP errors.
● Chaining Asynchronous Operations: Promises and async/await make it much
easier to chain asynchronous operations together in a sequence, compared to
nested callbacks (which can lead to "callback hell").
● Non-Blocking: Asynchronous operations allow your program to remain
responsive while waiting for long-running tasks to complete.
● Microtask Queue: Promises use a microtask queue, which has higher priority
than the message queue. This means that Promises are resolved before other
asynchronous tasks (like setTimeout callbacks).
● setImmediate() and process.nextTick() (Node.js): In Node.js, there are
additional ways to schedule asynchronous tasks:
○ setImmediate(callback): Schedules a callback function to be executed in the
next iteration of the event loop.
○ process.nextTick(callback): Schedules a callback function to be executed at
the end of the current event loop iteration, before any tasks in the message
queue. process.nextTick() callbacks are executed before setTimeout or
setImmediate callbacks.
6. Advanced Concepts
Once you have a solid understanding of the basics, you can delve into more advanced
JavaScript concepts. These concepts can help you write more efficient, maintainable,
and powerful code.
Closures
A closure is a function that "remembers" its lexical scope even when the function is
executed outside that scope. In other words, a closure allows a function to access
variables from its outer (enclosing) function's scope, even after the outer function has
finished executing.
● Lexical Scope: Lexical scope means that the scope of a variable is determined
by its position in the source code. Inner functions have access to variables
declared in their outer functions.
● How Closures Work: When a function is declared, it creates a closure over the
variables in its surrounding scope. These variables are stored in the function's
[[Environment]] record. When the function is executed, it can access these
variables.
● Uses of Closures:
○ Data hiding and encapsulation: Closures can be used to create private
variables and methods, preventing them from being accessed from outside
the function.
○ Creating function factories: Functions that return other functions, where
the returned functions "remember" the arguments passed to the factory
function.
○ Maintaining state in asynchronous operations: Closures can be used to
preserve variables across asynchronous callbacks.
function outerFunction(outerVar) {
const innerVar = "Inner variable";
function innerFunction() {
console.log("Outer variable:", outerVar);
console.log("Inner variable:", innerVar);
}
return innerFunction;
}
const myClosure = outerFunction("Hello from outer"); // myClosure now holds a
reference to innerFunction, and the closure allows it to access outerVar
myClosure(); // Output:
// Outer variable: Hello from outer
// Inner variable: Inner variable
function createCounter() {
let count = 0; // Private variable
return {
increment: function() {
count++;
console.log("Count:", count);
},
decrement: function() {
count--;
console.log("Count:", count);
},
getCount: function() {
return count;
}
};
}
Key Points:
● Closures are created when a function is declared, not when it's called.
● Closures can lead to memory leaks if you're not careful. If a closure references a
large object, that object will not be garbage collected until the closure itself is no
longer reachable.
● Closures are a powerful feature of JavaScript, but they can be tricky to
understand at first. It's important to visualize how the scope chain works and how
functions retain access to variables from their surrounding scopes.
Prototype and Inheritance
JavaScript is a prototype-based language, which means that objects inherit
properties and methods from other objects via a mechanism called the prototype
chain. This is different from classical inheritance, which is used in languages like Java
and C++.
● Prototypes: Every function in JavaScript (except for arrow functions, which don't
have their own this) has a prototype property. The prototype property is an
object. When you create an object using the new keyword with a constructor
function, the new object's internal [[Prototype]] property (accessible via
Object.getPrototypeOf(obj) or __proto__ - but the latter is deprecated) is set to
the constructor function's prototype object.
● Prototype Chain: When you try to access a property on an object, JavaScript
first looks for the property on the object itself. If it doesn't find the property, it
then looks for the property on the object's prototype. If it still doesn't find the
property, it continues to search up the prototype chain (the chain of prototypes)
until it reaches the end of the chain (which is null). This is how inheritance works
in JavaScript.
● Object.create(): The Object.create() method creates a new object with the
specified prototype object and properties. This provides a more flexible way to
create objects with specific prototypes.
● Constructor Functions: Constructor functions are functions that are designed
to be used with the new keyword to create objects. The this keyword inside a
constructor function refers to the newly created object. Constructor functions
are used to initialize the properties of the new object.
// Constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
// Using Object.create()
const animal = {
type: "animal",
eat: function() {
console.log("Eating...");
}
};
console.log(cat.name); // "Whiskers"
console.log(cat.breed); // "Siamese"
cat.eat();
Key Points:
● Every object in JavaScript inherits from Object.prototype (except for objects
created with Object.create(null), which have no prototype).
● When you define a method on a constructor function's prototype, all objects
created with that constructor function share the same method. This is more
efficient than defining the method directly on each object, as it saves memory.
● You can add properties and methods to built-in object prototypes (like
Array.prototype or String.prototype), but it's generally not recommended, as it
can lead to conflicts if other libraries or code also modify the same prototypes. If
you do, use caution and check if the property or method already exists before
adding it.
if (!Array.prototype.myMap) { // Check if myMap already exists
Array.prototype.myMap = function(callback) {
const newArray = [];
for (let i = 0; i < this.length; i++) {
newArray.push(callback(this[i], i, this));
}
return newArray;
};
}
Classes
Classes, introduced in ES6, provide a more structured and familiar way to create
objects and deal with inheritance in JavaScript. However, it's important to understand
that classes in JavaScript are syntactic sugar over the existing prototype-based
inheritance. They don't introduce a new inheritance model; they just provide a more
convenient syntax for working with prototypes.
● Class Declaration: Use the class keyword to declare a class.
● Constructor: The constructor() method is a special method within a class that is
called when you create a new object using the new keyword. It's used to initialize
the properties of the object.
● Methods: You define methods inside the class body. Methods are added to the
prototype of the class.
● Inheritance: Use the extends keyword to create a subclass (derived class) that
inherits from a superclass (base class).
● super(): Inside the constructor of a subclass, you must call super() before
accessing this. super() calls the constructor of the superclass and initializes the
this context. You can also use super to call methods on the superclass.
● static Methods: Static methods are defined on the class itself, not on the
prototype. You call them directly on the class (e.g., ClassName.staticMethod()),
not on instances of the class.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}