JavaScript

JavaScript is the backbone of modern web technologies, enabling interactive and dynamic user experiences. Below is a guide to the core JavaScript concepts essential for web development.

1. JavaScript Syntax

Syntax

JavaScript syntax refers to the rules for writing code. Key elements include:

Case Sensitivity

JavaScript is case-sensitive, meaning that variable names with different capitalization are treated as separate variables. For example:

let name = "John"; // 'name' with lowercase n
let Name = "Doe";  // 'Name' with uppercase N is different

Here, name and Name are different variables because JavaScript differentiates between uppercase and lowercase letters.

Semicolons

Semicolons are used to terminate statements. While JavaScript has Automatic Semicolon Insertion (ASI) which automatically inserts semicolons where they are needed, it's good practice to explicitly use them. This helps in avoiding errors or confusion in more complex code.

let age = 25;   // Use of semicolon for clarity
console.log(age);

Comments

Comments are used to add explanations to your code and improve readability. They do not get executed.

2. Variable Declarations: var vs let

In JavaScript, you can declare variables using either var or let. Both allow you to store data, but there are differences between them:

var Declaration

var was used to declare variables in JavaScript before ES6 (ECMAScript 2015). However, it has some issues that make it less useful compared to let.

var name = "Alice";
var name = "Bob";  // No error, redeclaration is allowed

let Declaration

let was introduced in ES6 and is preferred for declaring variables in modern JavaScript because it has block-level scope (only accessible within the block in which it is declared).

let age = 25;
let age = 30;  // Error: 'age' has already been declared

3. Data Types in JavaScript

JavaScript has several data types that can be categorized into two types: Primitive Data Types and Reference Data Types.

Primitive Data Types

Reference Data Types

Scope of Variables in JavaScript

In JavaScript, the scope of a variable refers to the region in which it is accessible. There are three main types of scope: Global Scope, Function ScopeBlock Scope.


1. Global Scope

A variable declared outside any function or block belongs to the Global Scope. It can be accessed from anywhere in the program, both inside and outside functions.

Example:

let globalVar = "I'm global"; // Declared outside any function

function showGlobalVar() {
    console.log(globalVar); // Accessible inside function
}

showGlobalVar(); // Outputs: I'm global
console.log(globalVar); // Outputs: I'm global, accessible outside function

In the above example:

2. Function Scope (Local Scope)

A variable declared inside a function using var, let, or const is only accessible within that function. This is called Function Scope or Local Scope.

Example:

function localScopeExample() {
    let localVar = "I'm local"; // Declared inside the function
    console.log(localVar); // Accessible inside function
}

localScopeExample(); // Outputs: I'm local
console.log(localVar); // Error: localVar is not defined

In the above example:

3. Block Scope

A variable declared inside a block (denoted by {}, such as inside loops or conditionals) using let or const is only accessible within that block. This is called Block Scope.

Example with let and const:

{
    let blockVar = "I'm block scoped"; // Inside a block
    console.log(blockVar); // Accessible inside block
}

console.log(blockVar); // Error: blockVar is not defined

In the above example:

Example with var:

{
    var blockVar = "I'm block scoped with var"; // Inside a block
    console.log(blockVar); // Accessible inside block
}

console.log(blockVar); // Accessible outside block because 'var' is function-scoped

In the above example:


Key Differences Between var, let, and const

Variable Type Function Scope Block Scope Hoisted
var ✅ Yes ❌ No ✅ Yes
let ✅ Yes ✅ Yes ❌ No
const ✅ Yes ✅ Yes ❌ No

Explanation:

Hoisting in JavaScript

Hoisting is a JavaScript behavior where variable declarations and function declarations are moved (or "hoisted") to the top of their containing scope before the code is executed. This means you can reference functions and variables before they are declared in the code. However, the way hoisting behaves can differ depending on whether you are using var, let, const, or function declarations.

How Hoisting Works

1. Hoisting of Variables Declared with var

When a variable is declared with var, it is hoisted to the top of its scope (global or function scope) and initialized with undefined.

You can reference the variable before its actual declaration, but the value will be undefined until the assignment occurs.

Example with var:

console.log(a); // ✅ Output: undefined (hoisted but not initialized)
var a = 10;
console.log(a); // Output: 10

Explanation: In this example, the declaration var a; is hoisted to the top, and a is initialized with undefined. The assignment a = 10; happens in place, and when a is logged before the assignment, it prints undefined.

2. Hoisting of Variables Declared with let and const

Variables declared with let and const are also hoisted, but they are not initialized. Trying to access them before declaration leads to a ReferenceError because they are in the "temporal dead zone" (TDZ) between the start of the block and the line where they are initialized.

Example with let and const:

console.log(b); // Error: Cannot access 'b' before initialization
let b = 20;

Explanation: In this example, b is hoisted but not initialized, leading to a ReferenceError if accessed before its declaration.

3. Hoisting of Function Declarations

Function declarations are fully hoisted, meaning both the function's definition and its body are moved to the top of the scope. This allows you to call a function before its declaration in the code.

Example with Function Declarations:

greet(); // ✅ Output: "Hello!"

function greet() {
    console.log("Hello!");
}

Explanation: The function greet() is fully hoisted, meaning you can call it before the function is defined in the code, and it will output Hello!.

Example with Function Expressions:

greet(); // Error: greet is not a function

var greet = function() {
    console.log("Hello!");
};

Explanation: When using a function expression, only the var greet; declaration is hoisted, not the assignment. Therefore, calling greet() before the function expression is assigned will result in an error.

Summary of Hoisting Behavior

Declaration Type Hoisted to Top? Initialization Access Before Declaration
var Yes Initialized as undefined Works, but value will be undefined
let and const Yes Not initialized (Temporal Dead Zone) Throws ReferenceError if accessed early
Function Declarations Yes Fully initialized (definition and body) Works, can be called before declaration
Function Expressions No Assigned to variable at runtime Cannot be called before assignment

Best Practices

Wrapper Types in JavaScript

In JavaScript, wrapper types (or wrapper objects) are special objects that provide methods and properties for the corresponding primitive types. Primitives such as String, Number, and Boolean are immutable, meaning their values cannot be changed once assigned. However, the wrapper objects allow us to treat these primitive values as objects, giving us the ability to access methods and properties associated with those types.

Main Advantages of Wrapper Objects

  • Access to Methods: Primitive types themselves do not have methods. Wrapper objects allow primitive values to have access to methods like .toUpperCase() for strings, .toFixed() for numbers, etc.
  • Object Behavior: Primitives are immutable and do not have the ability to be modified (e.g., no methods to change the string). The wrapper objects, however, allow methods to be invoked on them, even though the actual primitive remains unchanged.
  • Automatic Conversion: JavaScript automatically converts primitives to their corresponding wrapper objects when necessary. This means you can use a string, number, or boolean just like an object in certain situations without explicitly creating a wrapper.
  • Enhanced Functionality: While primitive values are limited to their data, wrappers provide enhanced functionality by exposing additional methods and properties for manipulation and interaction.

Wrapper Objects for Primitive Types

1. String Wrapper

The String wrapper is used to access string-related methods like .toUpperCase(), .slice(), etc. JavaScript automatically wraps primitive strings in String objects when methods are called.

let str = "Hello"; // primitive string
console.log(typeof(str)); // "string"

let strObj = new String("Hello"); // String wrapper object
console.log(typeof(strObj)); // "object"
console.log(strObj.toUpperCase()); // "HELLO" (method available on String object)

2. Number Wrapper

The Number wrapper allows us to work with numeric values as objects. It provides access to methods like .toFixed() and .toPrecision() for formatting numbers.

let n = 100; // primitive number
console.log(typeof(n)); // "number"

let nObj = new Number(100); // Number object wrapper
console.log(typeof(nObj)); // "object"
console.log(nObj.toFixed(2)); // "100.00" (method available on Number object)

3. Boolean Wrapper

The Boolean wrapper object allows us to call methods like .toString() on boolean values.

let bool = true; // primitive boolean
console.log(typeof(bool)); // "boolean"

let boolObj = new Boolean(true); // Boolean object wrapper
console.log(typeof(boolObj)); // "object"
console.log(boolObj.toString()); // "true" (method available on Boolean object)

4. BigInt Wrapper (ES6)

The BigInt type is used to handle large integers, exceeding the limit of regular numbers. You can use the BigInt wrapper to work with large integers, though it's typically unnecessary since BigInt literals can be used directly.

let bigInt = 12345678901234567890n; // BigInt literal (no need for wrapper)
console.log(typeof(bigInt)); // "bigint"

let bigIntObj = new BigInt(12345678901234567890); // BigInt object wrapper (not necessary)
console.log(typeof(bigIntObj)); // "object"

5. Symbol Wrapper (ES6)

Symbol is used to create unique identifiers. While it's generally not required to use a Symbol object wrapper, JavaScript provides the ability to create one if necessary.

let sym = Symbol("id"); // Symbol primitive
console.log(typeof(sym)); // "symbol"

let symObj = new Symbol("id"); // Symbol object wrapper (not necessary)
console.log(typeof(symObj)); // "object"

Summary of Wrapper Objects for Primitive Types:

Primitive Type Wrapper Object Example Description
String String let str = new String("Hello"); Allows string methods like .toUpperCase(), .toLowerCase()
Number Number let num = new Number(100); Allows number methods like .toFixed(), .toPrecision()
Boolean Boolean let bool = new Boolean(true); Allows boolean methods like .toString()
BigInt BigInt let bigInt = 123n; Allows handling large integers
Symbol Symbol let sym = Symbol("id"); Creates unique identifiers for objects

Important Notes:

  • Automatic Wrapping: JavaScript automatically wraps primitives in their corresponding wrapper objects when you call methods on them. For example, when you call str.toUpperCase(), JavaScript temporarily creates a String wrapper for the primitive string.
  • Performance Consideration: Wrapping primitives into objects can introduce overhead. It is generally recommended to use the primitive values directly unless you need to access methods and properties related to the primitive type.
  • Avoid Manual Wrapping: Although JavaScript allows you to manually create wrapper objects (e.g., new String("Hello")), it's not typically necessary. Using the primitive directly is simpler and avoids unnecessary complexity.

JavaScript Operators

1. Arithmetic Operators

Arithmetic operators are used to perform basic mathematical operations like addition, subtraction, multiplication, division, etc.

Operator Description Example Output
+ Addition let sum = 5 + 3; 8
- Subtraction let diff = 10 - 4; 6
* Multiplication let product = 4 * 2; 8
/ Division let division = 10 / 2; 5
% Modulo (Remainder) let remainder = 7 % 2; 1
++ Increment (adds 1) let count = 3; count++; 4
-- Decrement (subtracts 1) let count = 3; count--; 2

2. Comparison Operators

Comparison operators are used to compare two values and return a boolean value (`true` or `false`).

Operator Description Example Output
== Loose equality (compares values only) 5 == "5"; true
=== Strict equality (compares value and type) 5 === "5"; false
!= Loose inequality (compares values only) 5 != 3; true
!== Strict inequality (compares value and type) 5 !== "5"; true
> Greater than 5 > 3; true
< Less than 5 < 3; false
>= Greater than or equal to 5 >= 5; true
<= Less than or equal to 5 <= 3; false

3. Logical Operators

Logical operators are used to combine multiple conditions and return a boolean result.

Operator Description Example Output
&& Logical AND (both conditions must be true) true && false; false
|| Logical OR (at least one condition must be true) true || false; true
! Logical NOT (inverts the boolean value) !true; false

4. Assignment Operators

Assignment operators are used to assign values to variables and perform operations in a single step.

Operator Description Example Output
= Assigns a value to a variable let x = 10; x = 10
+= Adds and assigns x += 5; x = 15
-= Subtracts and assigns x -= 5; x = 5
*= Multiplies and assigns x *= 2; x = 30
/= Divides and assigns x /= 2; x = 15
%= Assigns the remainder of division x %= 2; x = 1

5. Other Operators

Ternary Operator

The ternary operator is a shorthand for an if-else statement.

let age = 18;
let canVote = age >= 18 ? "Yes" : "No"; // If age >= 18, return "Yes", otherwise "No"

typeof Operator

The typeof operator is used to determine the type of a variable or value.

typeof "hello";    // "string"
typeof 42;         // "number"
typeof true;       // "boolean"
typeof undefined;  // "undefined"
typeof null;       // "object" (this is a quirk in JavaScript)

JavaScript Control Flow

1. Conditional Statements

Conditional statements are used to execute code based on whether a condition is true or false.

if-else Statement

The if-else statement allows you to run a block of code if a specified condition is true, and optionally run another block if the condition is false.

let age = 20;
if (age >= 18) {
    console.log("Adult");
} else {
    console.log("Minor");
}

switch Statement

The switch statement evaluates an expression and matches it with corresponding case labels.

let day = "Monday";
switch (day) {
    case "Monday":
        console.log("Start of the week");
        break;
    case "Friday":
        console.log("Almost weekend");
        break;
    default:
        console.log("Regular day");
}

2. Loops

Loops are used to repeat a block of code multiple times until a certain condition is met.

for Loop

The for loop is used when you know the number of iterations you need.

for (let i = 0; i < 5; i++) {
    console.log(i);
}

while Loop

The while loop executes as long as a specified condition evaluates to true.

let i = 0;
while (i < 5) {
    console.log(i);
    i++;
}

do-while Loop

The do-while loop runs the block of code at least once before checking the condition.

let i = 0;
do {
    console.log(i);
    i++;
} while (i < 5);

3. Break and Continue

The break and continue statements control the flow inside loops.

break Statement

The break statement is used to exit a loop prematurely.

for (let i = 0; i < 5; i++) {
    if (i === 3) break; // Exits the loop when i is 3
    console.log(i);
}

continue Statement

The continue statement is used to skip the current iteration of a loop and continue with the next iteration.

for (let i = 0; i < 5; i++) {
    if (i === 3) continue; // Skips the iteration when i is 3
    console.log(i);
}

JavaScript Arrays

In JavaScript, an array is a special type of object used to store multiple values in a single variable. Arrays can hold various data types, including numbers, strings, objects, and even other arrays. Arrays also have the ability to dynamically resize as needed.

1. Creating Arrays

There are two common ways to create arrays in JavaScript:

Array Literal (Recommended)

The Array Literal syntax is the most common and recommended way to create an array. It uses square brackets [] to define the array elements.

let fruits = ["Apple", "Banana", "Orange"];

Using new Array() (Not Recommended)

The new Array() constructor is less commonly used. It allows you to create an array, but it can lead to some unexpected behavior if not used correctly. You can use it by passing elements as arguments, or by specifying the length of the array.

let colors = new Array("Red", "Green", "Blue");

Dynamic Array

You can also create an empty array and then assign values dynamically using indexing:

let a = [];
a[0] = 1;
a[1] = 2;
a[2] = 3;

2. Accessing Array Elements

Array elements in JavaScript are accessed using zero-based indexing, meaning the first element is at index 0.

let fruits = ["Apple", "Banana", "Orange"];

console.log(fruits[0]); // Output: Apple
console.log(fruits[1]); // Output: Banana
console.log(fruits[2]); // Output: Orange

3. Array Properties

Arrays have built-in properties that help you work with them more easily. One of the most commonly used properties is length, which returns the number of elements in the array.

let fruits = ["Apple", "Banana", "Orange"];
console.log(fruits.length); // Output: 3

4. Common Array Methods

JavaScript provides many built-in methods to manipulate arrays. Below are some of the most common ones:

MethodDescriptionExample
push()Adds an element to the end of an array.arr.push("Mango")
pop()Removes the last element of an array.arr.pop()
shift()Removes the first element of an array.arr.shift()
unshift()Adds an element to the beginning of an array.arr.unshift("Grapes")
indexOf()Finds the index of an element in the array.arr.indexOf("Banana")
includes()Checks if an element exists in the array.arr.includes("Apple")
splice()Adds or removes elements from an array at a specific index.arr.splice(1, 1, "Mango")
slice()Extracts a portion of an array.arr.slice(1, 3)
join()Converts an array into a string.arr.join(", ")
reverse()Reverses the order of the elements in the array.arr.reverse()
sort()Sorts the elements of an array.arr.sort()
map()Creates a new array by transforming each element.arr.map(x => x * 2)
filter()Creates a new array with elements that meet a condition.arr.filter(x => x > 10)
forEach()Iterates over each element of the array.arr.forEach(x => console.log(x))

Example Usage of Methods

let numbers = [10, 20, 30, 40];

// Add elements
numbers.push(50); // [10, 20, 30, 40, 50]
numbers.unshift(5); // [5, 10, 20, 30, 40, 50]

// Remove elements
numbers.pop(); // [5, 10, 20, 30, 40]
numbers.shift(); // [10, 20, 30, 40]

// Slice and Splice
let newNumbers = numbers.slice(1, 3); // [20, 30]
numbers.splice(2, 1, 25); // [10, 20, 25, 40]

// Iterating
numbers.forEach(num => console.log(num)); // Prints each number

// Mapping
let doubled = numbers.map(num => num * 2); // [20, 40, 50, 80]

console.log(numbers);
console.log(doubled);

5. Looping Through Arrays

There are several ways to loop through an array in JavaScript.

1. Using a for Loop

The traditional for loop can be used to iterate through an array using its length property.

let fruits = ["Apple", "Banana", "Orange"];

for (let i = 0; i < fruits.length; i++) {
    console.log(fruits[i]); // Prints each fruit
}

2. Using forEach()

The forEach() method executes a function on each element of the array.

let fruits = ["Apple", "Banana", "Orange"];

fruits.forEach(fruit => {
    console.log(fruit); // Prints each fruit
});

6. Multidimensional Arrays

You can create arrays inside arrays, known as multidimensional arrays or nested arrays.

let matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

console.log(matrix[1][2]); // Output: 6 (Accessing second row, third column)

7. Checking If a Variable is an Array

You can use Array.isArray() to check whether a variable is an array or not.

let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // Output: true

console.log(Array.isArray("Hello")); // Output: false

JavaScript Functions

A JavaScript function is a set of statements that are designed to perform a specific task or calculate a value. Functions help break down complex tasks into smaller, reusable, and more manageable pieces of code. Functions take input parameters, perform their designated task, and return an output.

1. How to Create Functions in JavaScript

To create a function in JavaScript, we follow this basic structure:

function functionName(parameters) {
    // function body
    // statements
}
  • function: The keyword used to define a function.
  • functionName: The name of the function. This name is used to identify and call the function.
  • parameters: Optional. These are the values passed into the function when it is invoked (also called arguments).
  • function body: This contains the statements (code) that define what the function does when it is called.

Example:

function greet() {
    console.log("Hello, World!");
}

In this example, greet is a named function that logs a greeting message to the console.

2. Types of Functions in JavaScript

A. Named Functions

Named functions are the most common type of functions in JavaScript. They are defined with a name that can be used to invoke them.

function add(a, b) {
    return a + b;
}
console.log(add(5, 3));  // Output: 8

In this case, the function add takes two parameters (a and b), adds them, and returns the result.

B. Arrow Functions

Arrow functions are a more concise way to write functions. They can be used for single expressions and are often used when the function body is small.

Arrow functions can either have an expression body or a block body.

Arrow Function with an Expression Body:

let multiply = (a, b) => a * b;

console.log(multiply(4, 5));  // Output: 20

Arrow Function with a Block Body:

let greet = (name) => {
    console.log("Hello, " + name + "!");
};
greet("Alice");  // Output: Hello, Alice!

C. Function Expressions

Function expressions assign a function to a variable. These functions can be anonymous or named, and the function's value is stored in the variable. You can call the function using the variable name.

let greet = function(name) {
    return 'Hello ' + name;
};
console.log(greet("Bob"));  // Output: Hello Bob

D. Anonymous Functions

Anonymous functions are those that are defined without a name. These are commonly used when functions are passed as arguments to other functions.

let sayHello = function() {
    console.log("Hello!");
};
sayHello();  // Output: Hello!

Anonymous functions can also be used with event handlers or other operations like callbacks.

3. Function Invocation

The code inside a function only executes when the function is called or invoked. This is how we invoke (call) a function in JavaScript:

greet();  // Call the function

When the function greet is invoked, the code inside the function body is executed, and any results (like a return value) can be used.

Example of Function Invocation:

function add(a, b) {
    return a + b;
}
let result = add(10, 5);
console.log(result);  // Output: 15

4. Function Hoisting

JavaScript has a behavior called hoisting, where function declarations are moved to the top of their containing scope before execution. This means you can call a function before it is defined in the code.

Example:

greet();  // This works due to hoisting

function greet() {
    console.log("Hello!");
}

Here, even though the greet function is called before it's declared, it works because JavaScript hoists the function declaration to the top of the scope.

5. Function Scope

In JavaScript, every time a function is created, it has its own scope. This means that variables declared inside a function are not accessible outside that function. The scope of a function is often referred to as local scope, while variables outside a function exist in the global scope.

Example:

function greet() {
    let message = "Hello!";
    console.log(message);  // Accessible inside the function
}
greet();  // Output: Hello!
console.log(message);  // Error: message is not defined (because it's outside the function)

In the above example, the variable message is accessible only inside the function greet. Outside of the function, it cannot be accessed because it's a local variable.

6. Returning Values from Functions

Functions can return a value using the return keyword. This allows functions to produce an output that can be used later in the program.

Example:

function add(a, b) {
    return a + b;
}
let result = add(5, 7);
console.log(result);  // Output: 12

In this example, the function add takes two parameters (a and b), adds them, and returns the result. The result is then stored in the variable result.

7. Function Parameters and Arguments

Functions can accept parameters, which are values passed into the function when it is called. These parameters allow functions to perform operations on different input values. When you call the function, you provide arguments for each parameter.

Example:

function multiply(a, b) {
    return a * b;
}
let product = multiply(3, 4);
console.log(product);  // Output: 12

Here, a and b are the parameters, and 3 and 4 are the arguments passed when calling the function.

Conclusion

JavaScript functions are powerful tools for organizing and reusing code. They allow you to break down tasks into manageable, self-contained blocks. Functions can be defined in several ways, each offering unique benefits. Understanding how to use functions effectively is crucial for writing clean, maintainable JavaScript code.

Separation of Concerns

The three main concerns in web application development are:

  • Content - The HTML document
  • Presentation - The CSS styles that specify how the document looks
  • Behavior - The JavaScript, which handles user interaction and dynamic changes to the document

Keeping the three concerns as separate as possible improves the delivery of the application to a variety of user agents, such as graphic browsers, text-only browsers, assistive technologies for disabled users, and mobile devices. It also aligns with the principle of progressive enhancement—starting with the basic HTML-only experience for simple user agents and progressively adding features for more capable browsers.

If the browser supports CSS, the user gets an enhanced presentation. If it supports JavaScript, additional interactive features improve the user experience.

In Practice: Separation of Concerns

In practice, applying the separation of concerns in web development involves the following steps:

  • Testing with CSS turned off: Ensure the page is still usable without the styles, with the content remaining readable and accessible.
  • Testing with JavaScript turned off: Verify that the page can still fulfill its main purpose, ensuring all links are functional (avoiding placeholder links like href="#"), and forms work and submit correctly.
  • Avoid inline event handlers and styles: Refrain from using inline event handlers (like onclick) or inline styles, as these violate the separation of concerns and mix behavior/presentation with content.
  • Use semantically meaningful HTML elements: Employ proper HTML elements (such as headings, paragraphs, and lists) to structure the content in a meaningful and accessible way.

These practices ensure that the page remains accessible, functional, and adaptable across different environments, enhancing user experience and supporting progressive enhancement.