docs

Arrays in JavaScript

Arrays are a type of object in JavaScript that allow you to store multiple values in a single variable. They are essential for organizing data, especially when dealing with collections of items, such as lists, sequences, or any set of values.


1. Creating an Array

You can create an array in JavaScript in several ways:

Example:

let fruits = ["Apple", "Banana", "Cherry"]; // Using array literal
let numbers = new Array(1, 2, 3, 4, 5); // Using Array constructor

2. Referring to Array Elements

You can access individual elements in an array using their index. In JavaScript, array indices are zero-based, meaning the first element is at index 0.

Syntax:

let element = arrayName[index];

Example:

let colors = ["Red", "Green", "Blue"];
console.log(colors[0]); // Outputs: Red
console.log(colors[1]); // Outputs: Green
console.log(colors[2]); // Outputs: Blue

3. Populating an Array

You can add elements to an array in several ways:


4. Array Transformations

JavaScript provides several methods for transforming arrays, including:


5. Sparse Arrays

Sparse arrays are arrays in which not all indices are assigned values. JavaScript allows you to create sparse arrays by skipping indices.

Example:

let sparseArray = [];
sparseArray[0] = "Hello";
sparseArray[2] = "World";
console.log(sparseArray); // Outputs: [ 'Hello', <1 empty item>, 'World' ]
console.log(sparseArray.length); // Outputs: 3

In this example, the array has a length of 3, but it contains only two defined elements.


6. Multi-Dimensional Arrays

Multi-dimensional arrays are arrays of arrays. They allow you to create complex data structures like matrices or grids.

Example:

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

console.log(matrix[0][0]); // Outputs: 1
console.log(matrix[1][1]); // Outputs: 5

In this example, matrix is a 2D array where each element is itself an array.


7. Using Arrays to Store Other Properties

Arrays can store elements of any data type, including other arrays, objects, or even functions.

Example:

let mixedArray = [
  "String",
  123,
  true,
  { name: "Alice" },
  [1, 2, 3],
  function () {
    return "Hello";
  },
];

console.log(mixedArray[3].name); // Outputs: Alice
console.log(mixedArray[4][1]); // Outputs: 2
console.log(mixedArray[5]()); // Outputs: Hello

This flexibility allows you to store diverse types of data in a single array.


8. Working with Array-Like Objects

Array-like objects are objects that have a length property and can be indexed but do not have array methods like push(), pop(), etc. Examples include the arguments object and NodeList returned by document.querySelectorAll().

To convert an array-like object into an array, you can use:


Summary

Keyed Collections in JavaScript

Keyed collections are data structures that store elements with unique keys for efficient retrieval. JavaScript offers two primary types of keyed collections: Maps and Sets. Each of these provides specific features and functionality that can be beneficial in various programming scenarios.


1. Maps

A Map is a collection of key-value pairs where both keys and values can be any type of object or primitive. Unlike regular JavaScript objects, which only allow strings or symbols as keys, a Map can use any data type as a key.

Creating a Map

You can create a Map by using the Map() constructor, optionally passing in an iterable object (such as an array) with key-value pairs to initialize it.

Syntax:

let map = new Map([
  [key1, value1],
  [key2, value2],
]);

Methods of a Map

Example:

let contacts = new Map();
contacts.set("John", "john@example.com");
contacts.set("Alice", "alice@example.com");

console.log(contacts.get("John")); // Outputs: john@example.com
console.log(contacts.has("Alice")); // Outputs: true
contacts.delete("John"); // Deletes the 'John' entry
console.log(contacts.size); // Outputs: 1

Iterating over a Map

You can iterate over a Map using several methods:

Example:

let map = new Map();
map.set("foo", 123);
map.set("bar", 456);

for (let [key, value] of map.entries()) {
  console.log(key, value); // Outputs: foo 123 and bar 456
}

2. Sets

A Set is a collection of unique values, meaning that no two elements in a Set can be the same. Sets are useful for ensuring that only distinct items are stored and for performing operations like union, intersection, and difference.

Creating a Set

You can create a Set by using the Set() constructor. An optional iterable object (such as an array) can be passed in to initialize the set with values.

Syntax:

let set = new Set([value1, value2, value3]);

Methods of a Set

Example:

let uniqueNumbers = new Set();
uniqueNumbers.add(1);
uniqueNumbers.add(2);
uniqueNumbers.add(2); // Won't add, as 2 is already in the set

console.log(uniqueNumbers.has(1)); // Outputs: true
console.log(uniqueNumbers.size); // Outputs: 2

uniqueNumbers.delete(1);
console.log(uniqueNumbers.size); // Outputs: 1

Iterating over a Set

You can iterate over a Set using:

Example:

let set = new Set([1, 2, 3, 4]);

for (let value of set) {
  console.log(value); // Outputs: 1, 2, 3, 4
}

3. Key and Value Equality in Maps and Sets

Map Key Equality

Set Value Equality


Summary

These collections help in efficient data storage and retrieval when working with data that involves unique keys or values.

Working with Classes in JavaScript

JavaScript classes provide a template for creating objects and working with object-oriented programming (OOP) concepts like inheritance, encapsulation, and polymorphism. They simplify the process of creating objects and managing shared behavior through methods.


1. Creating New Objects

In JavaScript, you can create objects in different ways, such as using object literals, constructors, or the class keyword.

Using Object Literals

Object literals are a quick and straightforward way to create a new object.

Syntax:

let objectName = {
  property1: value1,
  property2: value2,
};

Example:

let person = {
  name: "Alice",
  age: 25,
};
console.log(person.name); // Outputs: Alice

Using Constructors

A constructor function is a special type of function used to create and initialize objects. When called with the new keyword, a new object is created.

Syntax:

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

let alice = new Person("Alice", 25);
console.log(alice.name); // Outputs: Alice

Using Classes

A class is a more formal and structured way to create objects in JavaScript. It uses the class keyword and allows you to define the structure and behavior of objects.

Syntax:

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

let bob = new Person("Bob", 30);
console.log(bob.name); // Outputs: Bob

2. Objects and Properties

Objects are collections of properties. A property is an association between a key (name) and a value.

Adding Properties to Objects

You can add properties to an object either when creating it or afterward.

Example:

let car = {
  brand: "Toyota",
  model: "Corolla",
};

car.year = 2020; // Adding a new property after creation
console.log(car.year); // Outputs: 2020

Accessing Object Properties

You can access the properties of an object using either dot notation or bracket notation.

Example:

console.log(car.brand); // Outputs: Toyota (dot notation)
console.log(car["model"]); // Outputs: Corolla (bracket notation)

3. Inheritance

Inheritance allows one class to inherit the properties and methods of another. In JavaScript, you can use the extends keyword to inherit from another class.

Example of Inheritance:

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

let dog = new Dog("Rover");
dog.speak(); // Outputs: Rover barks.

Here, the Dog class inherits from the Animal class, but it overrides the speak() method to provide its specific behavior.


4. Defining Methods

Methods in JavaScript classes are defined inside the class body. These are functions that belong to objects created by that class.

Syntax:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

let john = new Person("John", 35);
john.greet(); // Outputs: Hello, my name is John.

5. Defining Getters and Setters

Getters and setters allow controlled access to an object’s properties. A getter method retrieves the value of an object’s property, while a setter method allows you to change its value.

Example of Getters and Setters:

class Person {
  constructor(name) {
    this._name = name; // _name is a private variable
  }

  get name() {
    return this._name;
  }

  set name(newName) {
    if (newName) {
      this._name = newName;
    } else {
      console.log("Invalid name");
    }
  }
}

let person = new Person("Alice");
console.log(person.name); // Outputs: Alice

person.name = "Bob"; // Using setter
console.log(person.name); // Outputs: Bob

In this example, name is accessed and modified using getter and setter methods.


6. Comparing Objects

In JavaScript, comparing two objects directly checks if they refer to the same memory location, not if they have the same properties or values.

Example:

let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };

console.log(obj1 === obj2); // Outputs: false, because obj1 and obj2 are different objects in memory

let obj3 = obj1;
console.log(obj1 === obj3); // Outputs: true, because obj1 and obj3 refer to the same object

If you want to compare objects based on their properties and values, you need to implement a custom comparison function.


Reference:

For more information, visit the MDN documentation: MDN: Working with Objects.

Using Classes in JavaScript

Classes in JavaScript were introduced in ECMAScript 6 (ES6) and provide a more straightforward and cleaner way to handle object-oriented programming (OOP). Classes allow you to define blueprints for creating objects and encapsulate data and behavior. While JavaScript classes work similarly to classes in other OOP languages, they are essentially syntactic sugar over the prototypal inheritance system that JavaScript is based on.


1. Overview of Classes

JavaScript classes offer a clear structure to define object properties and behaviors. They are used to encapsulate related data and methods in a single entity. Classes enable you to:

Basic Class Structure:

class MyClass {
  constructor() {
    // Constructor logic
  }

  method1() {
    // Instance method logic
  }
}

2. Declaring a Class

You can declare a class in JavaScript using the class keyword. Classes can contain constructors, methods, static properties, and fields.

Syntax:

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

Here, Person is a class, and the constructor is a special method used for initializing objects created from the class.


3. Constructor

The constructor method is a special method of a class. It is called when a new instance of the class is created. This method is used to initialize the object’s properties.

Example:

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

let myCar = new Car("Toyota", "Corolla");
console.log(myCar.brand); // Outputs: Toyota

In the above example, Car class has a constructor that initializes the brand and model properties when a new car is created.


4. Instance Methods

Instance methods are functions defined within a class that can be invoked by instances of the class. These methods can access and manipulate the properties of the class.

Example:

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

  bark() {
    console.log(`${this.name} is barking!`);
  }
}

let dog = new Dog("Max");
dog.bark(); // Outputs: Max is barking!

Here, the bark method is an instance method, which can be called on instances of the Dog class.


5. Private Fields

Private fields in JavaScript classes start with the # symbol and are only accessible within the class. This provides encapsulation, ensuring that certain fields cannot be accessed or modified from outside the class.

Example:

class BankAccount {
  #balance = 0;

  deposit(amount) {
    this.#balance += amount;
  }

  getBalance() {
    return this.#balance;
  }
}

let account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // Outputs: 100
console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class

In this example, the #balance field is private and cannot be accessed directly from outside the BankAccount class.


6. Accessor Fields (Getters and Setters)

Accessor fields allow you to define custom logic when getting or setting the value of a property. These are defined using the get and set keywords.

Getter:

The get method retrieves the value of a property.

Setter:

The set method allows you to define custom behavior when assigning a value to a property.

Example:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.width * this.height;
  }

  set height(newHeight) {
    if (newHeight > 0) {
      this.height = newHeight;
    } else {
      console.log("Height must be positive.");
    }
  }
}

let rect = new Rectangle(5, 10);
console.log(rect.area); // Outputs: 50

Here, the area is calculated dynamically via a getter, and the height is validated via a setter.


7. Public Fields

Public fields are properties that are declared inside the class but outside any methods, making them directly accessible.

Example:

class Car {
  color = "red"; // Public field

  constructor(brand) {
    this.brand = brand;
  }
}

let car = new Car("Toyota");
console.log(car.color); // Outputs: red

In this example, color is a public field and is directly accessible outside the class.


8. Static Properties

Static properties (or methods) belong to the class itself and not to any object instances. They are called on the class directly and cannot be accessed by instances of the class.

Example:

class MathUtils {
  static pi = 3.14159;

  static calculateArea(radius) {
    return MathUtils.pi * radius * radius;
  }
}

console.log(MathUtils.pi); // Outputs: 3.14159
console.log(MathUtils.calculateArea(5)); // Outputs: 78.53975

In this example, pi and calculateArea are static, and they are accessed directly through the class name MathUtils.


9. Extends and Inheritance

Inheritance is a core concept in OOP where one class (child or subclass) inherits the properties and methods of another class (parent or superclass). In JavaScript, inheritance is achieved using the extends keyword.

Example:

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

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name} barks.`);
  }
}

let dog = new Dog("Rex");
dog.speak(); // Outputs: Rex barks.

In this example, Dog inherits from Animal, but it overrides the speak method to provide its own implementation.


10. Why Classes?

Classes provide a more structured, readable, and concise way to create and manage objects in JavaScript. While JavaScript already has a powerful prototypal inheritance system, classes make working with OOP principles easier by simplifying syntax and improving clarity.

Key Benefits:


Reference:

For more information, visit the MDN documentation: MDN: Using Classes.

Using Promises in JavaScript

JavaScript Promises are used to handle asynchronous operations. A promise represents a value that may be available now, in the future, or never. They help deal with operations like network requests, file handling, or any task that takes time to complete without blocking the main thread.

This explanation will cover key concepts related to Promises, such as Chaining, Error handling, Composition, Cancellation, Creating a Promise around an old callback API, and Timing.


1. Chaining

Promise chaining allows you to perform a series of asynchronous operations, one after another. Each step of the chain waits for the previous one to complete. Each .then() call returns a new promise, allowing further chaining.

Example:

fetch("https://api.example.com/data")
  .then((response) => response.json()) // Parse JSON from the response
  .then((data) => {
    console.log(data);
    return fetch("https://api.example.com/other-data"); // Chain another request
  })
  .then((response) => response.json()) // Parse the second response
  .then((data) => console.log(data)) // Log the second data
  .catch((error) => console.error("Error:", error)); // Handle any errors in the chain

In this example, multiple asynchronous operations (fetching data from two URLs) are performed in sequence using chained .then() calls.


2. Error Handling

Error handling in promises is achieved using .catch(). Errors in any of the promise steps are caught by .catch(), making it easy to handle exceptions in asynchronous code.

Example:

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("There was a problem with the fetch operation:", error)
  );

Here, if any error occurs during the fetch operation (e.g., network issues or invalid JSON), the .catch() will handle it, preventing the application from crashing.

Chained Error Handling:

Errors are propagated down the chain, so you only need one .catch() at the end.


3. Composition

Promise composition refers to combining multiple promises into one, so that all promises are resolved (or rejected) together. JavaScript provides two key methods for composing promises:

Promise.all() Example:

let promise1 = fetch("https://api.example.com/data1");
let promise2 = fetch("https://api.example.com/data2");

Promise.all([promise1, promise2])
  .then((responses) => Promise.all(responses.map((r) => r.json())))
  .then((data) => console.log(data))
  .catch((error) => console.error("One of the promises failed:", error));

Here, Promise.all() waits for both promise1 and promise2 to resolve. If any promise fails, the .catch() will handle the error.

Promise.race() Example:

let promise1 = fetch("https://api.example.com/slow-data");
let promise2 = fetch("https://api.example.com/fast-data");

Promise.race([promise1, promise2])
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error("Error:", error));

With Promise.race(), whichever promise resolves (or rejects) first will win the race.


4. Cancellation

JavaScript promises themselves do not support cancellation natively. However, you can mimic cancellation using a combination of flags or other mechanisms like AbortController.

Example with AbortController:

const controller = new AbortController();
const signal = controller.signal;

fetch("https://api.example.com/data", { signal })
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => {
    if (error.name === "AbortError") {
      console.log("Fetch aborted");
    } else {
      console.error("Error:", error);
    }
  });

// To cancel the request
controller.abort();

In this example, AbortController allows us to cancel an ongoing fetch request by calling controller.abort().


5. Creating a Promise Around an Old Callback API

Many older JavaScript APIs use callbacks to handle asynchronous operations. You can “promisify” these APIs by wrapping them in a new Promise.

Example:

function oldApi(callback) {
  setTimeout(() => callback(null, "Success!"), 1000);
}

function promisifiedApi() {
  return new Promise((resolve, reject) => {
    oldApi((error, result) => {
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
}

promisifiedApi()
  .then((result) => console.log(result)) // Outputs: Success!
  .catch((error) => console.error(error));

Here, the oldApi function uses a callback. We wrap it inside a new promise, making it easier to work with using .then() and .catch().


6. Timing

Promises are often used to delay code execution or wait for a specific duration. You can use setTimeout() inside a promise to create a delay.

Example:

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

delay(2000).then(() => console.log("Executed after 2 seconds"));

In this example, the promise resolves after 2 seconds, allowing you to delay the execution of subsequent code.


Reference:

For more information, visit the MDN documentation: MDN: Using Promises.

JavaScript Typed Arrays

Typed Arrays in JavaScript provide a way to work with binary data in a more structured manner. They were introduced to efficiently handle and manipulate raw binary data, such as that used in Web APIs, network protocols, or multimedia applications.

A Typed Array in JavaScript is an array-like object that provides a mechanism for reading and writing raw binary data in memory buffers. These are useful when dealing with data streams like files or buffers that have a structured binary format.

Let’s dive into the key concepts:


1. Buffers

A buffer is an area of memory that stores raw binary data. In JavaScript, buffers are represented using ArrayBuffer objects. An ArrayBuffer is a generic, fixed-length buffer of raw binary data. However, an ArrayBuffer itself doesn’t allow you to directly manipulate the data in memory. To work with this data, you need to use views.

Example:

let buffer = new ArrayBuffer(16); // Create a buffer of 16 bytes
console.log(buffer.byteLength); // Outputs: 16

This code creates an ArrayBuffer that is 16 bytes long. This memory block can be accessed and modified via views.


2. Views

Views provide a way to interpret the binary data stored in an ArrayBuffer. Each view represents the data in the buffer in a different format, like integers, floats, or even characters. There are various TypedArray views in JavaScript, such as:

These views allow you to manipulate the underlying binary data in a structured format.

Example:

let buffer = new ArrayBuffer(16); // 16 bytes of memory
let int32View = new Int32Array(buffer); // Interpret buffer as 32-bit integers
int32View[0] = 42; // Set the first 32-bit integer to 42

console.log(int32View[0]); // Outputs: 42

In this example, Int32Array is a view that allows you to interpret the raw data in the buffer as 32-bit signed integers.


3. Web APIs Using Typed Arrays

Several Web APIs utilize typed arrays because of their efficient handling of binary data. Some common examples include:

Example in WebGL:

let vertices = new Float32Array([
  0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0,
]);

// Used in WebGL to handle vertex buffer data
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

Here, the Float32Array is used to store the vertex coordinates for rendering in WebGL.


4. Examples

Example 1: Modifying Buffer Data with Views

let buffer = new ArrayBuffer(8); // Create a buffer of 8 bytes
let int8View = new Int8Array(buffer); // Create a view as 8-bit signed integers

int8View[0] = 127; // Set the first byte
console.log(int8View[0]); // Outputs: 127

int8View[1] = -128; // Set the second byte
console.log(int8View[1]); // Outputs: -128

This example demonstrates how to create an 8-byte buffer and manipulate its contents using the Int8Array view.

Example 2: Working with Different Views on the Same Buffer

let buffer = new ArrayBuffer(16); // 16 bytes buffer
let int16View = new Int16Array(buffer); // View buffer as 16-bit integers
let float32View = new Float32Array(buffer); // View same buffer as 32-bit floats

int16View[0] = 42; // Set first 16-bit integer to 42
console.log(float32View[0]); // Outputs: some floating-point value

In this example, multiple views are applied to the same buffer. Changing the data in one view affects how it’s interpreted in the other view because they share the same underlying buffer.


Typed Arrays Overview

JavaScript provides several types of TypedArray views, each suited for different kinds of data. Here are some of the most common:

Typed arrays provide high performance because they offer a direct mapping to the underlying memory, making them ideal for performance-critical applications like games or image processing.


5. Typed Arrays and BigInts

JavaScript typed arrays also support BigInt types for handling large integers beyond the limits of standard integers.

These types allow you to store and manipulate very large integers safely in memory.

Example:

let buffer = new ArrayBuffer(16); // 16 bytes buffer
let bigIntView = new BigInt64Array(buffer); // View as 64-bit BigInts

bigIntView[0] = 12345678901234567890n; // BigInt value
console.log(bigIntView[0]); // Outputs: 12345678901234567890n

BigInt typed arrays allow for precise manipulation of very large numbers that are beyond the range of regular 64-bit integers.


Summary

JavaScript typed arrays provide a way to efficiently work with binary data using ArrayBuffer as the memory buffer and typed views like Int32Array or Float32Array to manipulate the data. These are widely used in various Web APIs for high-performance operations, such as WebGL, WebRTC, and handling multimedia files.

Reference

For more information, visit the MDN documentation: MDN: Typed Arrays.

JavaScript: Iterators and Generators

Iterators and Generators are powerful features in JavaScript that enable working with sequences of data. These concepts allow you to handle collections, control flow, and asynchronous behavior more efficiently.

This explanation will cover the concepts of Iterators, Generator functions, Iterables, and Advanced generators.


1. Iterators

An iterator is an object that allows traversal through a sequence of data, one step at a time. An iterator provides a .next() method that returns the next value in the sequence, along with a flag indicating whether the end of the sequence has been reached.

Creating an Iterator

You can create an iterator by defining an object with a .next() method. This method should return an object with two properties:

Example:

function createIterator(arr) {
  let index = 0;
  return {
    next: function () {
      if (index < arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    },
  };
}

let iterator = createIterator(["apple", "banana", "cherry"]);
console.log(iterator.next()); // { value: 'apple', done: false }
console.log(iterator.next()); // { value: 'banana', done: false }
console.log(iterator.next()); // { value: 'cherry', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

In this example, the iterator traverses through an array of fruits. Each call to .next() returns the next item in the sequence until the iterator is exhausted.


2. Generator Functions

A generator function is a special type of function that can pause and resume its execution. It returns an iterator, called a generator, that can yield multiple values during its execution.

You define a generator function using the function* syntax and use the yield keyword to yield values.

Example of a Generator Function:

function* fruitGenerator() {
  yield "apple";
  yield "banana";
  yield "cherry";
}

let fruitGen = fruitGenerator();
console.log(fruitGen.next()); // { value: 'apple', done: false }
console.log(fruitGen.next()); // { value: 'banana', done: false }
console.log(fruitGen.next()); // { value: 'cherry', done: false }
console.log(fruitGen.next()); // { value: undefined, done: true }

In this example, the generator function fruitGenerator() yields three values, one at a time. Each call to .next() resumes execution until the next yield is encountered.

Benefits of Generator Functions:


3. Iterables

An iterable is an object that implements the @@iterator method, which returns an iterator. This makes the object compatible with constructs like for...of loops and the spread operator (...).

Example of an Iterable:

let myIterable = {
  [Symbol.iterator]: function* () {
    yield "apple";
    yield "banana";
    yield "cherry";
  },
};

for (let fruit of myIterable) {
  console.log(fruit); // Logs: 'apple', 'banana', 'cherry'
}

In this example, myIterable is an object that defines its iterator using a generator function. This allows it to be used in a for...of loop to iterate over its values.

Built-in Iterables:

JavaScript has several built-in iterable objects, such as:

These objects are iterable by default, meaning they can be traversed using iterators.

Using Spread Operator with Iterables:

let fruits = ["apple", "banana", "cherry"];
let moreFruits = [...fruits, "date", "elderberry"];

console.log(moreFruits); // ['apple', 'banana', 'cherry', 'date', 'elderberry']

Here, the spread operator (...) is used to expand the iterable fruits into another array.


4. Advanced Generators

Advanced generators offer more sophisticated control over the generator’s behavior. These features include:

Passing Values into a Generator:

You can send values into a generator by passing arguments to the .next() method.

Example:

function* counter() {
  let count = 0;
  while (true) {
    count += yield count;
  }
}

let gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next(2).value); // 2
console.log(gen.next(3).value); // 5

In this example, the generator function counter() accepts values passed from .next() calls, allowing you to modify the count during iteration.

Error Handling in Generators:

You can inject errors into a generator using .throw() to simulate or handle exceptions.

Example:

function* generator() {
  try {
    yield "Start";
    yield "Continue";
  } catch (error) {
    console.log("Error caught:", error);
  }
  yield "End";
}

let gen = generator();
console.log(gen.next().value); // 'Start'
console.log(gen.throw(new Error("Oops!")).value); // 'Error caught: Oops!', 'End'

Here, an error is thrown into the generator function, which is caught by the try...catch block inside the generator.

Delegating to Another Generator:

The yield* expression allows a generator to delegate its execution to another generator or iterable.

Example:

function* inner() {
  yield "Inner 1";
  yield "Inner 2";
}

function* outer() {
  yield "Outer 1";
  yield* inner(); // Delegates to `inner`
  yield "Outer 2";
}

let gen = outer();
console.log(gen.next().value); // 'Outer 1'
console.log(gen.next().value); // 'Inner 1'
console.log(gen.next().value); // 'Inner 2'
console.log(gen.next().value); // 'Outer 2'

In this example, outer() delegates control to inner() using yield*. The values from inner() are seamlessly integrated into the outer() generator.


Summary

Reference:

For more detailed information, visit the MDN documentation: MDN: Iterators and Generators.

JavaScript: Meta-programming

Meta-programming refers to writing code that manipulates or enhances the behavior of other code at runtime. In JavaScript, meta-programming allows you to interact with the language’s behavior and structure using proxies and reflection.

Meta-programming enables you to define custom behavior for fundamental operations such as property access, assignment, or function invocation. Two key concepts used in JavaScript meta-programming are Proxies and Reflection.

Let’s go through these concepts in detail, covering:

  1. Proxies
  2. Handlers and traps
  3. Revocable Proxy
  4. Reflection

1. Proxies

A proxy in JavaScript allows you to intercept and redefine fundamental operations performed on objects. A proxy wraps an object and intercepts operations like reading or writing properties, calling methods, and more.

Proxies consist of two parts:

Basic Example of a Proxy:

let target = {
  message: "Hello World",
};

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

let proxy = new Proxy(target, handler);

console.log(proxy.message); // "Hello World"
console.log(proxy.nonExistentProp); // "Property not found!"

In this example, the proxy intercepts the get operation (property access). If the property exists on the target, it returns the value. Otherwise, it returns a custom message, “Property not found!”

Common Use Cases of Proxies:


2. Handlers and Traps

A handler is an object that defines the behavior for the proxy. It contains traps—functions that intercept operations performed on the target object.

Common Proxy Traps:

  1. get: Intercepts reading a property from the target.
  2. set: Intercepts writing a property value to the target.
  3. has: Intercepts the in operator (checking if a property exists in an object).
  4. deleteProperty: Intercepts deleting a property (delete operator).
  5. apply: Intercepts calling a function.
  6. construct: Intercepts using new to create instances from a constructor function.

Example of Using Traps:

let user = {
  name: "John",
  age: 25,
};

let handler = {
  set: function (obj, prop, value) {
    if (prop === "age" && value < 0) {
      throw new Error("Age must be a positive number");
    }
    obj[prop] = value;
    return true;
  },
};

let proxyUser = new Proxy(user, handler);
proxyUser.age = 30; // Works fine
// proxyUser.age = -5; // Throws Error: "Age must be a positive number"

In this example, the set trap ensures that only positive values are assigned to the age property.


3. Revocable Proxy

A revocable proxy is a special type of proxy that can be revoked, meaning the proxy becomes invalid and can no longer interact with the target object.

You create a revocable proxy using Proxy.revocable(), which returns an object with two properties:

Example of a Revocable Proxy:

let target = { greeting: "Hello" };

let { proxy, revoke } = Proxy.revocable(target, {});

console.log(proxy.greeting); // "Hello"
revoke(); // Proxy is now invalid
// console.log(proxy.greeting); // Throws TypeError: Cannot perform 'get' on a proxy that has been revoked

In this example, once revoke() is called, the proxy is no longer valid, and attempting to access the target object through the proxy will result in an error.

Use Cases for Revocable Proxies:


4. Reflection

The Reflection API in JavaScript provides methods that correspond to fundamental operations (such as getting, setting, or deleting properties) and are similar to proxy traps. This allows you to directly invoke low-level object operations in a controlled manner.

The Reflect object contains static methods that help simplify interaction with objects and proxies. Some of the most commonly used methods include:

Reflect Methods:

  1. Reflect.get(): Retrieves the value of a property from an object.
  2. Reflect.set(): Assigns a value to a property.
  3. Reflect.has(): Checks if an object has a property.
  4. Reflect.deleteProperty(): Deletes a property from an object.
  5. Reflect.apply(): Calls a function with a specific this value and arguments.

Example Using Reflect:

let person = { name: "John", age: 30 };

console.log(Reflect.get(person, "name")); // "John"
Reflect.set(person, "age", 31);
console.log(person.age); // 31

In this example, Reflect.get() retrieves a property value, and Reflect.set() assigns a new value to a property.

Using Reflect with Proxies:

You can combine Reflect with proxies to simplify trap behavior by delegating default operations to Reflect.

Example:

let handler = {
  get: function (target, prop) {
    console.log(`Accessing property: ${prop}`);
    return Reflect.get(target, prop); // Delegate to Reflect
  },
};

let proxy = new Proxy({ greeting: "Hello" }, handler);
console.log(proxy.greeting); // Logs: Accessing property: greeting, "Hello"

Here, the get trap logs property access and then uses Reflect.get() to retrieve the actual value.


Summary of Key Concepts

Why Use Meta-programming?

For more information, you can refer to the full documentation here: Meta-programming on MDN.

JavaScript Modules

JavaScript modules allow developers to split their code into separate files and reuse them across projects, improving organization and maintainability. The ES6 module system, introduced in 2015, offers a native way to define reusable components, functions, and variables, which can be imported or exported between different files.

Let’s explore the concepts in detail:


1. A Background on Modules

Modules in JavaScript solve the problem of namespace pollution and dependency management. Traditionally, developers used the IIFE (Immediately Invoked Function Expression) pattern or libraries like RequireJS to manage modules. ES6 introduced a standardized way to define and import/export code from different files.


2. Introducing an Example

A typical JavaScript module involves exporting features from one file and importing them into another.

For example, in module1.js, you might have:

export function greet() {
  return "Hello!";
}

In main.js, you import and use the function:

import { greet } from "./module1.js";

console.log(greet()); // "Hello!"

3. Basic Example Structure

To create a module, you need to:

Example of a Simple Module Structure:

File: mathUtils.js (module):

export const PI = 3.14159;

export function add(a, b) {
  return a + b;
}

File: main.js (where the module is used):

import { PI, add } from "./mathUtils.js";

console.log(PI); // 3.14159
console.log(add(5, 10)); // 15

4. Exporting Module Features

You can export features (variables, functions, classes) using two methods:

  1. Named exports: Export multiple items from a module.

    • Syntax: export { item1, item2 } or use export before declaration.
    • Example:
      export const name = "John";
      export function sayHi() {
        return "Hi!";
      }
      
  2. Default exports: Export a single feature from a module.

    • Syntax: export default item
    • Example:
      export default function greet() {
        return "Hello!";
      }
      

5. Importing Features into Your Script

You can import features from another module using the import keyword.

  1. Named imports:

    • Syntax: import { item1, item2 } from 'module'
    • Example:
      import { name, sayHi } from "./module1.js";
      
  2. Default imports:

    • Syntax: import defaultItem from 'module'
    • Example:
      import greet from "./module1.js";
      

6. Importing Modules Using Import Maps

Import maps allow you to control the module specifiers used when importing modules. This feature is useful for browsers to resolve module paths easily.

Example of an Import Map:

<script type="importmap">
  {
    "imports": {
      "utils": "/path/to/utils.js"
    }
  }
</script>

<script type="module">
  import { add } from "utils";
  console.log(add(1, 2));
</script>

Here, the import map specifies the module location for 'utils', allowing you to import it without needing to specify the full path every time.


7. Loading Non-JavaScript Resources

JavaScript modules can also interact with non-JavaScript resources like CSS or images using module loaders. Modern tools like Webpack and Rollup can bundle these resources and import them like modules.

For example:

import "./styles.css"; // Importing CSS into a module.

8. Applying the Module to Your HTML

You need to specify <script type="module"> when including a module in HTML.

<script type="module" src="./main.js"></script>

The type="module" attribute tells the browser to treat the script as a module, allowing it to use import and export.


9. Other Differences Between Modules and Classic Scripts


10. Default Exports vs Named Exports

Example:

// Default export
export default function () {
  console.log("I am default!");
}

// Named exports
export const PI = 3.14;
export function greet() {
  console.log("Hello!");
}

11. Avoiding Naming Conflicts

If multiple modules export features with the same name, you can avoid naming conflicts by renaming imports or exports.

import { greet as greetFromModule1 } from "./module1.js";
import { greet as greetFromModule2 } from "./module2.js";

12. Renaming Imports and Exports

You can rename features during import/export using the as keyword.

Renaming exports:

export { item1 as renamedItem1 };

Renaming imports:

import { item1 as alias } from "./module.js";

13. Creating a Module Object

You can import all exports of a module into an object using the * wildcard:

import * as math from "./mathUtils.js";
console.log(math.add(2, 3)); // Access add() function via the math object

14. Modules and Classes

Modules can work seamlessly with classes. You can define and export a class, then import it in another module.

Example:

// car.js
export class Car {
  constructor(brand) {
    this.brand = brand;
  }
  drive() {
    console.log(this.brand + " is driving!");
  }
}

// main.js
import { Car } from "./car.js";
const myCar = new Car("Tesla");
myCar.drive(); // "Tesla is driving!"

15. Aggregating Modules

Modules can re-export features from other modules without importing them first.

// re-export everything from module1 and module2
export * from "./module1.js";
export * from "./module2.js";

16. Dynamic Module Loading

With the import() function, you can load modules dynamically at runtime. This is particularly useful for loading resources conditionally or on demand.

Example:

import("./module.js").then((module) => {
  module.doSomething();
});

17. Top-level Await

JavaScript modules allow you to use await at the top level, without needing to wrap it inside an async function.

Example:

const response = await fetch("/data.json");
const data = await response.json();

18. Import Declarations are Hoisted

All import declarations are hoisted to the top of their module, meaning you can use imported items throughout your code, regardless of where the import statement appears.


19. Cyclic Imports

When modules import each other in a loop, this is known as cyclic imports. JavaScript handles this by providing a partially constructed module if one of the modules isn’t fully initialized yet.


20. Authoring “Isomorphic” Modules

Isomorphic modules can run both in the browser and in Node.js. You can achieve this by ensuring your module has no platform-specific code, or by using conditional logic to handle platform differences.


21. Troubleshooting

Common problems with modules include:

For more detailed information, refer to JavaScript Modules on MDN.