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.
You can create an array in JavaScript in several ways:
Using Array Literals: This is the most common and straightforward method.
Syntax:
let arrayName = [element1, element2, element3];
Using the Array Constructor: This method allows for the creation of an array using the Array
constructor.
Syntax:
let arrayName = new Array(element1, element2, element3);
Creating an Empty Array: You can also create an empty array, which you can populate later.
Example:
let emptyArray1 = []; // Using literal
let emptyArray2 = new Array(); // Using constructor
let fruits = ["Apple", "Banana", "Cherry"]; // Using array literal
let numbers = new Array(1, 2, 3, 4, 5); // Using Array constructor
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];
let colors = ["Red", "Green", "Blue"];
console.log(colors[0]); // Outputs: Red
console.log(colors[1]); // Outputs: Green
console.log(colors[2]); // Outputs: Blue
You can add elements to an array in several ways:
Direct Assignment: Assign a value to a specific index.
Example:
let numbers = [];
numbers[0] = 10; // Assigning to index 0
numbers[1] = 20; // Assigning to index 1
Using the push()
Method: Adds one or more elements to the end of the array.
Example:
let fruits = ["Apple", "Banana"];
fruits.push("Cherry"); // Now fruits is ['Apple', 'Banana', 'Cherry']
Using the unshift()
Method: Adds one or more elements to the beginning of the array.
Example:
fruits.unshift("Mango"); // Now fruits is ['Mango', 'Apple', 'Banana', 'Cherry']
JavaScript provides several methods for transforming arrays, including:
map()
: Creates a new array with the results of calling a provided function on every element in the array.
Example:
let numbers = [1, 2, 3];
let doubled = numbers.map((x) => x * 2); // Outputs: [2, 4, 6]
filter()
: Creates a new array with all elements that pass the test implemented by the provided function.
Example:
let numbers = [1, 2, 3, 4, 5];
let evens = numbers.filter((x) => x % 2 === 0); // Outputs: [2, 4]
reduce()
: Executes a reducer function on each element of the array, resulting in a single output value.
Example:
let numbers = [1, 2, 3, 4];
let sum = numbers.reduce((acc, curr) => acc + curr, 0); // Outputs: 10
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.
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.
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.
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:
Array.from()
: Creates a new array from an array-like or iterable object.
Example:
function example() {
console.log(arguments); // Arguments object
let argsArray = Array.from(arguments); // Converts to array
console.log(argsArray); // Outputs: [1, 2, 3]
}
example(1, 2, 3);
Spread Syntax (...
): Expands an iterable into a list of elements.
Example:
let nodeList = document.querySelectorAll("div");
let divArray = [...nodeList]; // Converts NodeList to Array
Array
constructor.push()
, or unshift()
.map()
, filter()
, and reduce()
for transformations.Array.from()
or spread syntax.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.
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.
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],
]);
set(key, value)
: Adds or updates the key-value pair in the map.get(key)
: Retrieves the value associated with the specified key.has(key)
: Checks if a specific key exists in the map.delete(key)
: Removes the specified key and its associated value.clear()
: Removes all key-value pairs from the map.size
: Returns the number of key-value pairs.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
You can iterate over a Map
using several methods:
map.keys()
: Returns an iterator for all the keys in the map.map.values()
: Returns an iterator for all the values in the map.map.entries()
: Returns an iterator for key-value pairs (default for for...of
).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
}
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.
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]);
add(value)
: Adds a value to the set.has(value)
: Checks if a specific value exists in the set.delete(value)
: Removes a specific value from the set.clear()
: Removes all values from the set.size
: Returns the number of values in the set.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
You can iterate over a Set
using:
set.keys()
: Returns an iterator for the values (same as set.values()
).set.values()
: Returns an iterator for the values in the set.set.entries()
: Returns an iterator for value-value pairs (useful for compatibility with Maps).let set = new Set([1, 2, 3, 4]);
for (let value of set) {
console.log(value); // Outputs: 1, 2, 3, 4
}
In Maps, key equality is based on the “same-value-zero” algorithm, which is similar to the strict equality (===
) operator, with one exception: NaN
is considered equal to NaN
(which is not the case for ===
).
Example:
let map = new Map();
map.set(NaN, "not-a-number");
console.log(map.get(NaN)); // Outputs: not-a-number
Two object references are considered different keys, even if the objects have identical properties and values.
Example:
let obj1 = { a: 1 };
let obj2 = { a: 1 };
let map = new Map();
map.set(obj1, "object one");
console.log(map.has(obj2)); // Outputs: false, because obj1 and obj2 are different references
In Sets, value equality also uses the “same-value-zero” algorithm, where NaN
is equal to NaN
, and two object references are not considered equal unless they reference the same object.
Example:
let set = new Set();
set.add(NaN);
console.log(set.has(NaN)); // Outputs: true
let obj1 = { a: 1 };
let obj2 = { a: 1 };
set.add(obj1);
console.log(set.has(obj2)); // Outputs: false, because obj1 and obj2 are different references
NaN
is considered equal to itself, but two objects are only equal if they reference the same object.These collections help in efficient data storage and retrieval when working with data that involves unique keys or values.
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.
In JavaScript, you can create objects in different ways, such as using object literals, constructors, or the class
keyword.
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
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
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
Objects are collections of properties. A property is an association between a key (name) and a value.
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
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)
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.
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.
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.
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.
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.
In JavaScript, comparing two objects directly checks if they refer to the same memory location, not if they have the same properties or values.
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.
For more information, visit the MDN documentation: MDN: Working with Objects.
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.
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
}
}
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.
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.
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.
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.
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.
The get
method retrieves the value of a property.
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.
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.
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
.
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.
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.
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:
For more information, visit the MDN documentation: MDN: Using Classes.
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.
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.
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.
Errors are propagated down the chain, so you only need one .catch()
at the end.
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()
: Waits for all promises to be fulfilled or for any one to be rejected.Promise.race()
: Waits for the first promise to settle (either resolved or rejected).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.
JavaScript promises themselves do not support cancellation natively. However, you can mimic cancellation using a combination of flags or other mechanisms like AbortController
.
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()
.
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()
.
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.
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.
For more information, visit the MDN documentation: MDN: Using Promises.
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:
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.
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.
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:
Int8Array
for 8-bit signed integers.Uint8Array
for 8-bit unsigned integers.Float32Array
for 32-bit floating-point numbers.Uint16Array
, Uint32Array
, etc.These views allow you to manipulate the underlying binary data in a structured format.
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.
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.
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.
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.
JavaScript provides several types of TypedArray views, each suited for different kinds of data. Here are some of the most common:
Int8Array
and Uint8Array
: 8-bit signed and unsigned integers, respectively.Int16Array
and Uint16Array
: 16-bit signed and unsigned integers.Int32Array
and Uint32Array
: 32-bit signed and unsigned integers.Float32Array
and Float64Array
: 32-bit and 64-bit floating-point numbers.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.
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.
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.
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.
For more information, visit the MDN documentation: MDN: Typed Arrays.
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.
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.
You can create an iterator by defining an object with a .next()
method. This method should return an object with two properties:
value
: The next value in the sequence.done
: A boolean indicating if the sequence is finished (true
if done, false
otherwise).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.
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.
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.
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 (...
).
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.
JavaScript has several built-in iterable objects, such as:
These objects are iterable by default, meaning they can be traversed using iterators.
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.
Advanced generators offer more sophisticated control over the generator’s behavior. These features include:
.next()
..throw()
.yield*
expression, you can delegate control to another generator function.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.
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.
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.
.next()
to move through the data.@@iterator
method, allowing them to be iterated over using constructs like for...of
.For more detailed information, visit the MDN documentation: MDN: Iterators and Generators.
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:
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:
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!”
A handler is an object that defines the behavior for the proxy. It contains traps—functions that intercept operations performed on the target object.
get
: Intercepts reading a property from the target.set
: Intercepts writing a property value to the target.has
: Intercepts the in
operator (checking if a property exists in an object).deleteProperty
: Intercepts deleting a property (delete
operator).apply
: Intercepts calling a function.construct
: Intercepts using new
to create instances from a constructor function.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.
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:
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.
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.get()
: Retrieves the value of a property from an object.Reflect.set()
: Assigns a value to a property.Reflect.has()
: Checks if an object has a property.Reflect.deleteProperty()
: Deletes a property from an object.Reflect.apply()
: Calls a function with a specific this
value and arguments.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.
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.
get
, set
, apply
, and construct
.Reflect
API provides low-level methods for manipulating objects and simplifying trap logic when working with proxies.For more information, you can refer to the full documentation here: Meta-programming on MDN.
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:
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.
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!"
To create a module, you need to:
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
You can export features (variables, functions, classes) using two methods:
Named exports: Export multiple items from a module.
export { item1, item2 }
or use export
before declaration.export const name = "John";
export function sayHi() {
return "Hi!";
}
Default exports: Export a single feature from a module.
export default item
export default function greet() {
return "Hello!";
}
You can import features from another module using the import
keyword.
Named imports:
import { item1, item2 } from 'module'
import { name, sayHi } from "./module1.js";
Default imports:
import defaultItem from 'module'
import greet from "./module1.js";
Import maps allow you to control the module specifiers used when importing modules. This feature is useful for browsers to resolve module paths easily.
<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.
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.
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
.
this
in modules: In modules, this
is undefined
at the top level. In classic scripts, it refers to the global object.defer
attribute on classic scripts.Example:
// Default export
export default function () {
console.log("I am default!");
}
// Named exports
export const PI = 3.14;
export function greet() {
console.log("Hello!");
}
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";
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";
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
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!"
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";
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();
});
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();
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.
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.
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.
Common problems with modules include:
./
) or absolute.For more detailed information, refer to JavaScript Modules on MDN.