JavaScript has 8 data types divided into primitive and reference types. Each type serves a specific purpose and has its own behavior in memory management.
// Primitive Types:
let string = "hello"; // Stores text data
let number = 42; // Stores numbers (both integers and floats)
let boolean = true; // Stores true/false values
let undefined; // Variable declared but not assigned
let nullValue = null; // Intentional absence of value
let symbol = Symbol(); // Unique identifier
let bigInt = 9007199254740991n; // Large numbers
// Reference Type:
let object = {}; // Stores collections of data and more complex entities
Scope determines the accessibility of variables in your code. Understanding scope is crucial for preventing variable leaks and managing data flow.
var globalVar = "Accessible everywhere"; // Global scope
function exampleFunction() {
var functionVar = "Only inside function"; // Function scope
if (true) {
let blockVar = "Only in this block"; // Block scope
const alsoBlockScoped = "Also block"; // Block scope
}
}
Hoisting moves declarations to the top of their scope during execution. This behavior affects how we should declare and initialize variables.
// Variable hoisting
console.log(hoistedVar); // undefined
var hoistedVar = 5; // Declaration is hoisted, assignment isn't
// Function hoisting
sayHello(); // Works! Function declarations are fully hoisted
function sayHello() {
console.log("Hello!");
}
Closures are functions that remember their outer scope variables. They're essential for data privacy and maintaining state.
function createCounter() {
let count = 0; // Private variable
return {
increment: () => ++count, // Has access to count
getCount: () => count // Has access to count
};
}
'this' refers to the current execution context and changes based on how a function is called. Its value depends on whether you use regular functions, arrow functions, or methods.
const obj = {
name: "Object",
// Regular function: 'this' refers to obj
regularMethod() {
console.log(this.name); // "Object"
},
// Arrow function: 'this' refers to outer scope
arrowMethod: () => {
console.log(this.name); // undefined
}
};
Arrow functions provide a shorter syntax for function expressions. They don't bind their own 'this', making them ideal for callbacks.
// Traditional vs Arrow
function traditional(a, b) { return a + b; }
const arrow = (a, b) => a + b; // Shorter syntax
// Arrow with single parameter and block
const square = x => {
return x * x; // Can use blocks when needed
};
Template literals allow embedded expressions and multi-line strings. They make string interpolation more readable and maintainable.
const name = "John";
// String interpolation
const greeting = `Hello, ${name}!`;
// Multi-line string
const multiLine = `
This is easier
to read than
concatenation
`;
Destructuring extracts values from objects or arrays into distinct variables. It simplifies working with complex data structures and function parameters.
// Object destructuring
const person = { name: "John", age: 30 };
const { name, age } = person; // Extract properties
// Array destructuring
const [first, second] = [1, 2]; // Extract array elements
Spread expands elements while rest collects them into an array. These operators simplify working with arrays and function arguments.
// Spread: expand elements
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // [1, 2, 3, 4]
// Rest: collect elements
function sum(...numbers) { // Collect all arguments
return numbers.reduce((a, b) => a + b);
}
Classes provide a cleaner syntax for object-oriented programming. They simplify inheritance and constructor patterns.
class Animal {
constructor(name) { // Constructor for initialization
this.name = name;
}
speak() { // Method definition
return `${this.name} makes a sound`;
}
}
Promises handle asynchronous operations with cleaner syntax than callbacks. Async/await further simplifies working with promises using synchronous-looking code.
// Promise creation and handling
const promise = new Promise((resolve, reject) => {
// Async operation here
resolve("Success!");
});
// Async/await usage
async function getData() {
try {
const result = await promise;
return result;
} catch (error) {
console.error(error);
}
}
The event loop handles asynchronous operations in JavaScript. It manages execution order of tasks, microtasks, and rendering.
console.log('First'); // Synchronous
Promise.resolve().then(() => { // Microtask
console.log('Microtask');
});
setTimeout(() => { // Macrotask
console.log('Timeout');
}, 0);
Objects inherit properties and methods from other objects through prototypes. This is JavaScript's fundamental mechanism for code reuse.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() { // Shared method
return `${this.name} speaks`;
};
const dog = new Animal("Dog"); // Inherits speak
JavaScript automatically manages memory through garbage collection. Understanding this helps prevent memory leaks and optimize performance.
// Memory leak example
function createNodes() {
const nodes = [];
for (let i = 0; i < 10000; i++) {
nodes.push(new Array(10000)); // Large arrays
}
return nodes; // Holds reference
}
Design patterns are reusable solutions to common programming problems. They provide tested templates for solving issues in software design.
// Singleton pattern
const Singleton = (function() {
let instance;
return {
getInstance: function() {
if (!instance) {
instance = {};
}
return instance;
}
};
})();
Browser Web APIs extend JavaScript's capabilities for web development. They provide interfaces for DOM manipulation, network requests, and more.
// Fetch API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
// LocalStorage API
localStorage.setItem('key', 'value');
const value = localStorage.getItem('key');
Proper error handling ensures robust and maintainable applications. Try-catch blocks and error objects help manage runtime errors.
try {
// Risky operation
throw new Error('Something went wrong');
} catch (error) {
console.error(error.message); // Handle error
} finally {
// Always executes
console.log('Cleanup');
}
Modules organize code into reusable, encapsulated pieces. They help maintain clean code structure and manage dependencies.
// math.js
export const add = (a, b) => a + b; // Named export
// main.js
import { add } from './math.js'; // Named import
console.log(add(2, 3)); // Use imported function
Higher-order functions are functions that take other functions as arguments or return functions as their result. They are fundamental in functional programming.
function greet(name) {
return `Hello, ${name}!`;
}
function processUserInput(callback) {
const name = "Alice";
console.log(callback(name)); // Passes greet function as a callback
}
processUserInput(greet); // Outputs: "Hello, Alice!"
Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data.
- Immutability: Data cannot be modified after it's created.
- First-Class Functions: Functions can be passed as arguments, returned from other functions, and assigned to variables.
const numbers = [1, 2, 3, 4, 5];
// Using map (higher-order function) to create a new array without modifying the original
const squares = numbers.map(num => num * num);
console.log(squares); // Outputs: [1, 4, 9, 16, 25]
console.log(numbers); // Outputs: [1, 2, 3, 4, 5] (original array remains unchanged)
Debouncing and throttling are techniques used to optimize performance in situations where a function is called repeatedly, such as during window resizing or scrolling.
Delays the processing of a function until a certain time has passed since the last time it was invoked.
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
const logResize = debounce(() => {
console.log('Resized window!');
}, 200);
window.addEventListener('resize', logResize);
Ensures that a function is only called at most once in a specified time period.
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
if (!lastRan) {
func.apply(this, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
const logScroll = throttle(() => {
console.log('Scrolled!');
}, 1000);
window.addEventListener('scroll', logScroll);
JavaScript handles asynchronous operations using callbacks, promises, and async/await. Understanding these concepts helps manage asynchronous code more effectively.
Functions that are passed as arguments to other functions and are executed after the completion of the outer function.
function fetchData(callback) {
setTimeout(() => {
callback("Data received");
}, 1000);
}
fetchData(data => {
console.log(data); // Outputs: "Data received"
});
Objects that represent the eventual completion (or failure) of an asynchronous operation and its resulting value.
const fetchDataPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
fetchDataPromise
.then(data => console.log(data)) // Outputs: "Data received"
.catch(error => console.error(error));
Syntactic sugar on top of promises that allows asynchronous code to be written in a more synchronous manner.
async function fetchDataAsync() {
const data = await fetchDataPromise; // Waits for the promise to resolve
console.log(data); // Outputs: "Data received"
}
fetchDataAsync();
The Fetch API provides a modern way to make HTTP requests in the browser. It returns promises and is much cleaner than the older XMLHttpRequest.
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch error:', error));
The bind()
method creates a new function that, when called, has its this
keyword set to a provided value.
const person = {
name: "Alice",
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
const greetAlice = person.greet.bind(person);
greetAlice(); // Outputs: "Hello, my name is Alice"
Both are used for timing functions, but they have different behaviors.
Executes a function once after a specified delay.
setTimeout(() => {
console.log('Executed after 2 seconds');
}, 2000);
Repeatedly calls a function with a fixed time delay between each call.
const intervalId = setInterval(() => {
console.log('Executed every second');
}, 1000);
// Clear the interval after 5 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log('Interval cleared');
}, 5000);
Certainly! Here’s an expanded README file that includes additional JavaScript topics such as hoisting, currying, and more, along with examples.
Hoisting is JavaScript's behavior of moving declarations to the top of the current scope during the compilation phase. This applies to both variable declarations (with var
, let
, and const
) and function declarations.
console.log(myVar); // Outputs: undefined (not a ReferenceError)
var myVar = 5;
foo(); // Outputs: "Hello, World!"
function foo() {
console.log("Hello, World!");
}
- The variable
myVar
is hoisted, but only the declaration is moved, not the assignment. Hence, it logsundefined
. - Function declarations are fully hoisted, allowing them to be called before their definition in the code.
Currying is a functional programming technique where a function with multiple arguments is transformed into a sequence of functions, each taking a single argument.
function add(a) {
return function(b) {
return a + b;
};
}
const addFive = add(5); // Returns a function that adds 5
console.log(addFive(3)); // Outputs: 8
console.log(add(10)(2)); // Outputs: 12
- The
add
function is curried, allowing partial application of its parameters.
A closure is a function that retains access to its outer lexical scope, even when the function is executed outside that scope.
function makeCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // Outputs: 1
console.log(counter()); // Outputs: 2
- The inner function retains access to the
count
variable even aftermakeCounter
has finished executing.
The this
keyword refers to the context in which a function is executed. Its value depends on how a function is called.
const person = {
name: "Alice",
greet() {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // Outputs: "Hello, my name is Alice"
const greetFunction = person.greet;
greetFunction(); // Outputs: "Hello, my name is undefined"
- When called as a method on
person
,this
refers toperson
. - When called as a standalone function,
this
refers to the global object (orundefined
in strict mode).
The spread operator (...
) allows an iterable (like an array) to be expanded into individual elements. Rest parameters allow functions to accept an indefinite number of arguments as an array.
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // Outputs: [1, 2, 3, 4, 5, 6]
function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3, 4)); // Outputs: 10
The module pattern is a design pattern used to create private and public encapsulation for variables and functions.
const counterModule = (function() {
let count = 0; // Private variable
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
})();
console.log(counterModule.increment()); // Outputs: 1
console.log(counterModule.getCount()); // Outputs: 1
console.log(counterModule.decrement()); // Outputs: 0
- The
count
variable is private and can only be modified via the public methods.
These methods provide powerful ways to manipulate arrays.
Creates a new array by applying a function to each element of the original array.
const numbers = [1, 2, 3];
const squares = numbers.map(num => num * num);
console.log(squares); // Outputs: [1, 4, 9]
Creates a new array containing elements that satisfy a specified condition.
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Outputs: [2]
Executes a reducer function on each element of the array, resulting in a single output value.
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // Outputs: 6
Promises allow for easier handling of asynchronous operations, and you can chain them for sequential execution.
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data received");
}, 1000);
});
fetchData
.then(data => {
console.log(data); // Outputs: "Data received"
return "Next data";
})
.then(nextData => {
console.log(nextData); // Outputs: "Next data"
})
.catch(error => console.error("Error:", error));
The Fetch API is a modern interface for making HTTP requests.
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Fetch error:', error));