Skip to content

Latest commit

ย 

History

History
1083 lines (764 loc) ยท 26 KB

File metadata and controls

1083 lines (764 loc) ยท 26 KB

Back to Javascript Home Page

๐Ÿง  Advanced Topics

These give deeper understanding of how JavaScript works and are often asked in interviews:


Asynchronous JavaScript

JavaScript is single-threaded, but it can handle asynchronous operations using callbacks, promises, and async/await. Letโ€™s explore these concepts. ๐Ÿ”„โณ

๐Ÿ”น Callbacks

A callback is a function passed as an argument to another function and executed later.

function fetchData(callback) {
  setTimeout(() => {
    callback("Data fetched!");
  }, 1000);
}

fetchData((data) => {
  console.log(data); // Data fetched!
});
  • Use Case: Handling asynchronous tasks like API calls or timers.

๐Ÿ”น Promises

A promise represents a value that may be available now, in the future, or never. It has three states: pending, fulfilled, and rejected.

const fetchData = new Promise((resolve, reject) => {
  setTimeout(() => resolve("Data fetched!"), 1000);
});

fetchData
  .then((data) => console.log(data)) // Data fetched!
  .catch((error) => console.error(error));
  • Use Case: Chaining asynchronous operations.

๐Ÿ”น async/await

async/await simplifies working with promises by allowing you to write asynchronous code that looks synchronous.

async function fetchData() {
  const data = await new Promise((resolve) => setTimeout(() => resolve("Data fetched!"), 1000));
  console.log(data);
}

fetchData(); // Data fetched!
  • Use Case: Cleaner and more readable asynchronous code.

Back to Top


JavaScript Event Loop

The event loop is what allows JavaScript to perform non-blocking operations, even though itโ€™s single-threaded. Letโ€™s break it down. ๐Ÿ”„๐Ÿ•’

๐Ÿ”น Call Stack

The call stack is where JavaScript keeps track of function calls.

function first() {
  console.log("First");
}

function second() {
  console.log("Second");
}

first();
second();
// Output: First, Second

๐Ÿ”น Callback Queue

The callback queue holds tasks (e.g., setTimeout) that are ready to be executed after the call stack is empty.

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

console.log("End");
// Output: Start, End, Timeout

๐Ÿ”น Microtask Queue

The microtask queue (e.g., Promise.then) has higher priority than the callback queue.

console.log("Start");

Promise.resolve().then(() => console.log("Promise"));

console.log("End");
// Output: Start, End, Promise

๐Ÿ”น Task Prioritization

Tasks in the microtask queue are executed before tasks in the callback queue.

Back to Top


Memory Management

JavaScript automatically manages memory using garbage collection, but understanding how it works can help you avoid memory leaks. ๐Ÿง ๐Ÿ’พ

๐Ÿ”น Garbage Collection

JavaScript uses a mark-and-sweep algorithm to remove objects that are no longer reachable.

let obj = { name: "Alice" };
obj = null; // The object is eligible for garbage collection

๐Ÿ”น Memory Leaks

Memory leaks occur when objects are not properly released.

  • Common Causes:
    • Global variables
    • Event listeners not removed
    • Closures holding references
let leaks = [];
function addLeak() {
  leaks.push(new Array(1000000).fill("leak"));
}
  • Solution: Use tools like Chrome DevTools to monitor memory usage.

Back to Top


Prototypal Inheritance

JavaScript uses prototypal inheritance, where objects inherit properties and methods from other objects. ๐Ÿงฌ๐Ÿ”—

๐Ÿ”น Object.create()

You can create an object with a specific prototype using Object.create().

const parent = { greet: () => console.log("Hello!") };
const child = Object.create(parent);

child.greet(); // Hello!

๐Ÿ”น __proto__ and prototype

  • __proto__: Refers to the prototype of an object.
  • prototype: Refers to the prototype of a constructor function.
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function () {
  console.log(`Hello, I'm ${this.name}`);
};

const alice = new Person("Alice");
alice.greet(); // Hello, I'm Alice

๐Ÿ”น Class Syntax

The class syntax is a cleaner way to implement inheritance.

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }

  study() {
    console.log(`${this.name} is studying.`);
  }
}

const bob = new Student("Bob", "A");
bob.greet(); // Hello, I'm Bob
bob.study(); // Bob is studying.

Back to Top


The new Keyword

The new keyword is used to create an instance of an object from a constructor function. ๐Ÿ› ๏ธ๐Ÿ†•

function Person(name) {
  this.name = name;
}

const alice = new Person("Alice");
console.log(alice.name); // Alice
  • How it works:
    1. Creates a new empty object.
    2. Sets the prototype of the object to the constructorโ€™s prototype.
    3. Executes the constructor function with this bound to the new object.
    4. Returns the new object.

Back to Top


bind, call, apply

These methods allow you to control the value of this in a function. ๐Ÿ”—๐Ÿ–‡๏ธ

๐Ÿ”น bind()

Returns a new function with this bound to a specific object.

const person = { name: "Alice" };
function greet() {
  console.log(`Hello, ${this.name}`);
}

const boundGreet = greet.bind(person);
boundGreet(); // Hello, Alice

๐Ÿ”น call()

Calls a function with this set to a specific object and arguments passed individually.

greet.call(person); // Hello, Alice

๐Ÿ”น apply()

Calls a function with this set to a specific object and arguments passed as an array.

greet.apply(person); // Hello, Alice

Back to Top


Custom Error Handling

JavaScript provides tools for handling errors gracefully. ๐Ÿ›ก๏ธโš ๏ธ

๐Ÿ”น try, catch, finally, throw

try {
  throw new Error("Something went wrong!");
} catch (error) {
  console.error(error.message); // Something went wrong!
} finally {
  console.log("Cleanup code here.");
}
  • Use Case: Handling runtime errors without crashing the application.

Back to Top


IIFE (Immediately Invoked Function Expression)

An IIFE is a function that runs immediately after it is defined. ๐Ÿ”„โšก

(function () {
  console.log("IIFE executed!");
})();
  • Use Case: Creating a private scope to avoid polluting the global namespace.

Back to Top


Module Bundlers & Tools

Modern JavaScript applications often use tools like module bundlers and transpilers to manage dependencies and ensure compatibility across different environments. Letโ€™s explore two key tools: Webpack and Babel. ๐Ÿ› ๏ธ๐Ÿ“ฆ

๐Ÿ”น Webpack

Webpack is a module bundler that takes your JavaScript files (and other assets) and bundles them into a single file (or multiple files) for use in the browser.

Key Features:
  1. Code Splitting: Breaks your code into smaller chunks for better performance.
  2. Loaders: Allows you to process files like CSS, images, and TypeScript.
  3. Plugins: Extend Webpackโ€™s functionality (e.g., minification, environment variables).
Example Webpack Configuration:
// webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js", // Entry point
  output: {
    filename: "bundle.js", // Output file
    path: path.resolve(__dirname, "dist"), // Output directory
  },
  module: {
    rules: [
      {
        test: /\.js$/, // Process .js files
        exclude: /node_modules/,
        use: {
          loader: "babel-loader", // Use Babel for transpiling
        },
      },
    ],
  },
};
  • Use Case: Bundling JavaScript files, optimizing assets, and managing dependencies.

๐Ÿ”น Babel

Babel is a JavaScript transpiler that converts modern JavaScript (ES6+) into older versions (ES5) for compatibility with older browsers.

Key Features:
  1. Transpilation: Converts ES6+ syntax to ES5.
  2. Plugins: Add features like JSX support or experimental syntax.
  3. Presets: Predefined configurations for specific environments (e.g., @babel/preset-env).
Example Babel Configuration:
// .babelrc
{
  "presets": ["@babel/preset-env"]
}
Using Babel with Webpack:
npm install --save-dev babel-loader @babel/core @babel/preset-env
  • Use Case: Ensuring compatibility with older browsers and environments.

Back to Top


Event-driven Architecture

Event-driven architecture is a design pattern where the flow of the program is determined by events such as user actions, sensor outputs, or messages from other programs. It is widely used in JavaScript, especially in the browser and Node.js. ๐Ÿ”„๐Ÿ“ก

๐Ÿ”น How It Works

  1. Event Emitters: Components emit events when something happens.
  2. Event Listeners: Other components listen for these events and respond accordingly.
Example in Node.js:
const EventEmitter = require("events");

const emitter = new EventEmitter();

// Register an event listener
emitter.on("data", (message) => {
  console.log(`Received: ${message}`);
});

// Emit an event
emitter.emit("data", "Hello, World!");
  • Use Case: Building scalable and decoupled systems.

๐Ÿ”น Advantages:

  1. Decoupling: Components donโ€™t need to know about each other.
  2. Scalability: Easy to add new event listeners or emitters.
  3. Asynchronous: Handles non-blocking operations efficiently.

๐Ÿ”น Common Use Cases:

  • User interactions in the browser (e.g., clicks, keypresses).
  • Server-side events in Node.js (e.g., HTTP requests, file I/O).
  • Real-time applications (e.g., chat apps, notifications).

Back to Top


Custom Events

Custom events allow you to define and dispatch your own events in JavaScript, enabling more flexible and modular code. ๐Ÿ”ง๐Ÿ“ข

๐Ÿ”น Creating and Dispatching Custom Events

You can create custom events using the CustomEvent constructor and dispatch them using dispatchEvent.

// Create a custom event
const event = new CustomEvent("myEvent", {
  detail: { message: "Hello, World!" }, // Pass custom data
});

// Add an event listener
document.addEventListener("myEvent", (e) => {
  console.log(e.detail.message); // Hello, World!
});

// Dispatch the event
document.dispatchEvent(event);

๐Ÿ”น Use Cases:

  1. Decoupling Components: Allow different parts of your application to communicate without direct dependencies.
  2. Custom Interactions: Trigger custom logic based on user actions or application state.

๐Ÿ”น Example: Custom Events in a Button Click

const button = document.querySelector("#myButton");

// Create and dispatch a custom event
button.addEventListener("click", () => {
  const customEvent = new CustomEvent("buttonClicked", {
    detail: { clickedAt: new Date() },
  });
  button.dispatchEvent(customEvent);
});

// Listen for the custom event
button.addEventListener("buttonClicked", (e) => {
  console.log(`Button clicked at: ${e.detail.clickedAt}`);
});

๐Ÿ”น Advantages:

  • Flexibility: Define events specific to your applicationโ€™s needs.
  • Reusability: Components can emit and listen for events without tight coupling.

Back to Top


Throttling & Debouncing

These techniques optimize performance by controlling how often a function is executed. ๐Ÿ•’โšก

๐Ÿ”น Throttling

Ensures a function is called at most once in a specified time period.

function throttle(func, limit) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= limit) {
      lastCall = now;
      func(...args);
    }
  };
}

๐Ÿ”น Debouncing

Ensures a function is called only after a specified delay has passed since the last call.

function debounce(func, delay) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), delay);
  };
}

Back to Top


Service Workers & Web APIs

Service Workers and Web APIs are essential for building modern, performant, and feature-rich web applications. Letโ€™s explore their roles. ๐ŸŒโš™๏ธ

๐Ÿ”น Service Workers

A Service Worker is a script that runs in the background, separate from the main browser thread, enabling features like offline support, caching, and push notifications.

Key Features:
  1. Offline Support: Cache assets to make your app work offline.
  2. Push Notifications: Send notifications even when the app is not open.
  3. Background Sync: Sync data in the background.
Example: Registering a Service Worker
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("/service-worker.js")
    .then(() => console.log("Service Worker registered"))
    .catch((error) => console.error("Service Worker registration failed:", error));
}
Example: Caching Files in a Service Worker
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open("v1").then((cache) => {
      return cache.addAll(["/index.html", "/styles.css", "/script.js"]);
    })
  );
});

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});
  • Use Case: Progressive Web Apps (PWAs) for offline functionality and better performance.

๐Ÿ”น Web APIs

Web APIs are built into the browser and provide functionality for interacting with the browser and the web.

Examples of Web APIs:
  1. Fetch API: For making HTTP requests.

    fetch("https://api.example.com/data")
      .then((response) => response.json())
      .then((data) => console.log(data))
      .catch((error) => console.error(error));
  2. Geolocation API: For accessing the userโ€™s location.

    navigator.geolocation.getCurrentPosition((position) => {
      console.log(position.coords.latitude, position.coords.longitude);
    });
  3. Web Storage API: For storing data in the browser.

    localStorage.setItem("key", "value");
    console.log(localStorage.getItem("key")); // value
  4. WebSockets API: For real-time communication.

    const socket = new WebSocket("wss://example.com/socket");
    socket.onmessage = (event) => console.log(event.data);

Back to Top


Security Concepts

Security is critical in web development. Letโ€™s explore two common vulnerabilities: XSS and CSRF. ๐Ÿ›ก๏ธ๐Ÿ”’

๐Ÿ”น XSS (Cross-Site Scripting)

XSS occurs when an attacker injects malicious scripts into a website, which are then executed in the browser of other users.

Example of XSS:
<!-- Vulnerable Code -->
<input type="text" id="input" />
<div id="output"></div>
<script>
  document.getElementById("input").addEventListener("input", (e) => {
    document.getElementById("output").innerHTML = e.target.value; // Vulnerable to XSS
  });
</script>
Prevention:
  1. Escape User Input: Use libraries like DOMPurify.
    const sanitized = DOMPurify.sanitize(userInput);
  2. Content Security Policy (CSP): Restrict the sources of scripts.
  3. Avoid innerHTML: Use textContent instead.

๐Ÿ”น CSRF (Cross-Site Request Forgery)

CSRF tricks a user into performing actions they didnโ€™t intend, such as transferring money or changing account settings.

Example of CSRF:

A malicious website sends a request to https://bank.com/transfer using the victimโ€™s session.

Prevention:
  1. CSRF Tokens: Include a unique token in forms.
  2. SameSite Cookies: Restrict cookies to same-site requests.
    Set-Cookie: sessionId=abc123; SameSite=Strict

Back to Top


JavaScript Patterns

Design patterns provide reusable solutions to common problems in software design. Letโ€™s explore some popular JavaScript patterns. ๐Ÿ› ๏ธ๐Ÿ“

๐Ÿ”น Module Pattern

Encapsulates code in a function to create private variables and methods.

const Module = (() => {
  const privateVar = "I am private";

  return {
    publicMethod: () => console.log(privateVar),
  };
})();

Module.publicMethod(); // I am private

๐Ÿ”น Revealing Module Pattern

Exposes only specific methods and properties.

const RevealingModule = (() => {
  let privateVar = "I am private";

  const privateMethod = () => console.log(privateVar);

  return {
    publicMethod: privateMethod,
  };
})();

RevealingModule.publicMethod(); // I am private

๐Ÿ”น Singleton Pattern

Ensures a class has only one instance.

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

๐Ÿ”น Observer Pattern

Allows objects to subscribe to and react to events.

class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  notify(data) {
    this.observers.forEach((observer) => observer(data));
  }
}

const subject = new Subject();
subject.subscribe((data) => console.log(`Observer 1: ${data}`));
subject.subscribe((data) => console.log(`Observer 2: ${data}`));

subject.notify("Event triggered!");

๐Ÿ”น Factory Pattern

Creates objects without specifying their exact class.

class Car {
  constructor(model) {
    this.model = model;
  }
}

class CarFactory {
  createCar(model) {
    return new Car(model);
  }
}

const factory = new CarFactory();
const car = factory.createCar("Tesla");
console.log(car.model); // Tesla

Back to Top


Functional Programming in JS

Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data. ๐Ÿงฎ๐Ÿ”—

๐Ÿ”น Pure Functions

A pure function always produces the same output for the same input and has no side effects.

const add = (a, b) => a + b;
console.log(add(2, 3)); // 5

๐Ÿ”น Immutability

Avoid modifying existing data; instead, create new data.

const arr = [1, 2, 3];
const newArr = [...arr, 4];
console.log(newArr); // [1, 2, 3, 4]

๐Ÿ”น Higher-Order Functions

A higher-order function takes a function as an argument or returns a function.

const multiply = (factor) => (num) => num * factor;
const double = multiply(2);
console.log(double(5)); // 10

Back to Top


Currying & Partial Application

Currying and partial application are techniques for transforming functions to make them more reusable and composable. ๐Ÿ”„๐Ÿ› ๏ธ

๐Ÿ”น Currying

Transforms a function with multiple arguments into a sequence of functions, each taking a single argument.

const add = (a) => (b) => a + b;
console.log(add(2)(3)); // 5

๐Ÿ”น Partial Application

Pre-fixes some arguments of a function, creating a new function with fewer arguments.

const multiply = (a, b) => a * b;
const double = multiply.bind(null, 2);
console.log(double(5)); // 10

Back to Top


Generators and Iterators

Generators and iterators provide a way to handle sequences of data. ๐Ÿ”„๐Ÿ”—

๐Ÿ”น Generators

A generator is a function that can pause and resume execution.

function* generator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3

๐Ÿ”น Iterators

An iterator is an object that defines a sequence and a way to access its elements.

const iterable = [1, 2, 3];
const iterator = iterable[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Back to Top


Symbols & Iterables

Symbols are a unique and immutable primitive data type introduced in ES6. They are often used as unique property keys, while iterables provide a way to define custom iteration behavior. ๐Ÿ”‘๐Ÿ”„

๐Ÿ”น Symbols

A Symbol is a unique and immutable value.

const sym1 = Symbol("description");
const sym2 = Symbol("description");

console.log(sym1 === sym2); // false
  • Use Case: Creating unique property keys to avoid naming collisions.
const obj = {
  [Symbol("id")]: 1,
  name: "Alice",
};

console.log(Object.keys(obj)); // ["name"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(id)]

๐Ÿ”น Iterables

An iterable is an object that implements the Symbol.iterator method, which returns an iterator.

const iterable = {
  [Symbol.iterator]() {
    let step = 0;
    return {
      next() {
        step++;
        if (step <= 3) {
          return { value: step, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

for (const value of iterable) {
  console.log(value); // 1, 2, 3
}
  • Use Case: Customizing iteration behavior for objects.

Back to Top


Proxy and Reflect API

The Proxy and Reflect APIs allow you to intercept and customize operations on objects. ๐Ÿ›ก๏ธ๐Ÿ”

๐Ÿ”น Proxy

A Proxy wraps an object and intercepts operations like property access, assignment, and function calls.

const target = { name: "Alice" };

const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : "Property not found";
  },
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Alice
console.log(proxy.age); // Property not found
  • Use Case: Validating or logging property access, creating reactive objects.

๐Ÿ”น Reflect

The Reflect API provides methods for performing object operations, similar to those intercepted by Proxy.

const obj = { name: "Alice" };

Reflect.set(obj, "age", 25);
console.log(Reflect.get(obj, "age")); // 25
  • Use Case: Simplifying object operations and ensuring consistency with Proxy.

๐Ÿ”น Proxy and Reflect Together

You can use Proxy and Reflect together to customize and delegate operations.

const target = { name: "Alice" };

const handler = {
  set(obj, prop, value) {
    if (prop === "age" && typeof value !== "number") {
      throw new TypeError("Age must be a number");
    }
    return Reflect.set(obj, prop, value);
  },
};

const proxy = new Proxy(target, handler);

proxy.age = 30; // Works
console.log(proxy.age); // 30

proxy.age = "thirty"; // โŒ Throws TypeError

Back to Top


Typed Arrays & Buffers

Typed arrays and buffers provide a way to handle binary data in JavaScript. They are essential for working with raw data, such as files, streams, or Web APIs. ๐Ÿ“ฆ๐Ÿ’พ

๐Ÿ”น Typed Arrays

Typed arrays are array-like objects that provide a way to read and write binary data.

const buffer = new ArrayBuffer(16); // Create a buffer of 16 bytes
const int32View = new Int32Array(buffer);

int32View[0] = 42;
console.log(int32View[0]); // 42
  • Use Case: Handling binary data efficiently, such as in WebGL or file processing.

๐Ÿ”น DataView

A DataView provides a low-level interface for reading and writing multiple types of data in an ArrayBuffer.

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);

view.setInt8(0, 127);
console.log(view.getInt8(0)); // 127
  • Use Case: Reading and writing data with different types and endianness.

๐Ÿ”น Buffers in Node.js

In Node.js, Buffer is used to handle binary data.

const buffer = Buffer.from("Hello, World!");
console.log(buffer.toString()); // Hello, World!
  • Use Case: Working with file systems, streams, and network protocols.

๐Ÿง  Summary

Feature Purpose Use Case
ArrayBuffer Fixed-length binary data buffer Base for typed arrays and DataView
Typed Arrays Array-like views of binary data Efficient binary data manipulation
DataView Flexible binary data access Mixed data types in a buffer
Buffer (Node) Binary data in Node.js File systems, streams, networking

Back to Top