AltSchool Of Engineering Tinyuka’24 Month 5 Week 2


This week, we kicked things off by reviewing our previous session, as we usually do. If you missed it, you can catch up here. After that, we dove right into this week's topic; Prototypes and Inheritance in JavaScript. Let's explore these fundamental concepts together!
Prototypes and Inheritance
In JavaScript, prototypes are a core feature that allows objects to inherit properties and methods from other objects. This concept is known as prototypal inheritance. For example, if you have a constructor function Animal, you can create an instance dog that inherits from Animal via its prototype (Animal.prototype). This creates a prototype chain where dog can access properties and methods defined in Animal.
Native Prototypes and Prototype Methods
JavaScript provides native prototypes for built-in objects like Array and Function. For instance, you can add custom methods to the Array.prototype, allowing all array instances to use this new method.
Class Basic Syntax
With the introduction of ES6, JavaScript supports class syntax, making it easier to create objects and handle inheritance. A basic class definition looks like this:
class Animal {
constructor(name) {
this.name = name;
}
}
Class Inheritance
Classes can extend other classes, allowing for a clear inheritance structure. For example:
class Dog extends Animal {
bark() {
console.log(`${this.name} says woof!`);
}
}
Static Properties and Methods
Classes can also define static properties and methods that belong to the class itself, rather than instances. For instance:
class Animal {
static kingdom = 'Animalia';
}
Private and Protected Properties and Methods
ES2022 introduced private properties and methods in classes, denoted with a # prefix. For example:
class Animal {
#age;
constructor(name, age) {
this.name = name;
this.#age = age;
}
}
These private members cannot be accessed outside the class.
Extending Built-in Classes
You can extend built-in classes like Array or Error to create specialized versions. For instance:
class CustomArray extends Array {
customMethod() {
// Custom functionality
}
}
Class Checking Instanceof and Mixins
To check if an object is an instance of a class, you can use the instanceof operator. For example:
let dog = new Dog('Buddy');
console.log(dog instanceof Animal); // true
Mixins can be used to share functionality across classes without traditional inheritance, allowing for flexible code reuse.
Error Handling Overview
In programming, errors can arise from various sources, leading to unexpected behavior. JavaScript provides a robust mechanism for managing these errors through the try...catch statement, which allows developers to handle exceptions without stopping the execution of the script.
Structure of Try Catch
The try...catch statement consists of a try block that contains code which might throw an error, followed by a catch block that executes if an error occurs. Additionally, a finally block can be included, which runs regardless of whether an error was thrown or not.
Execution Flow
Try Block: Code that may cause an error is placed inside the try block.
Catch Block: If an error occurs, control is transferred to the catch block, where you can handle the error gracefully.
Finally Block: The finally block, if present, executes after the try and catch, ensuring that cleanup code runs.
Example
Here’s a simple example demonstrating the use of try...catch:
try {
let result = riskyFunction(); // Function that may throw an error
console.log(result);
} catch (error) {
console.error("An error occurred:", error.message);
} finally {
console.log("Cleanup actions can be performed here.");
}
In this example:
If riskyFunction() throws an error, the catch block logs the error message.
Regardless of whether an error occurs, the finally block executes, allowing for any necessary cleanup.
Synchronous Nature
The try...catch statement operates synchronously, which means it will execute the code in a linear fashion, ensuring that errors are caught and handled in order.
Understanding Catch Binding
When an error occurs within the try block of a try...catch statement, the catch block is triggered. This block receives the error object, often referred to as exceptionVar (commonly named err), which contains critical information about the error.
Error Object Properties
The error object provides valuable details, including:
Message: A description of the error.
Type: The type of error that occurred.
Stack: A stack trace (though non-standard, it is widely supported), which aids in debugging by showing the call path leading to the error.
Destructuring the Error Object
You can use destructuring to extract multiple properties from the error object in a concise manner. This allows for cleaner code and easier access to the necessary information.
Example
Here’s an example demonstrating catch binding and destructuring:
try {
// Code that may throw an error
let result = riskyOperation();
} catch ({ message, name, stack }) {
console.error(`Error: ${name} - ${message}`);
console.error(`Stack trace: ${stack}`);
}
In this example:
When an error occurs in riskyOperation(), the catch block destructures the error object, directly accessing message, name, and stack.
This not only simplifies the code but also enhances readability, making it easier to log and debug errors.
Using catch binding effectively helps developers manage exceptions and gather meaningful insights into errors, improving the overall robustness of applications.
Creating Custom Errors
In JavaScript, you can enhance error handling by creating custom error types through the built-in Error class. This approach allows developers to define errors that are more meaningful and specific to their application’s context.
Benefits of Custom Errors
Custom errors enable you to provide tailored messages and additional properties, making it easier to understand the nature of the error when it occurs. This specificity is particularly useful when you need to catch and handle different error scenarios distinctly.
Basic Implementation
To create a custom error, you simply extend the Error class. Here’s a straightforward example:
class ValidationError extends Error {
constructor(message) {
super(message); // Call the parent constructor
this.name = "ValidationError"; // Set the error name
}
}
// Usage example
try {
throw new ValidationError("Invalid input data.");
} catch (error) {
console.error(`${error.name}: ${error.message}`);
}
In this example:
ValidationError is a custom error class that extends Error.
When an instance of ValidationError is thrown, it carries a specific message and has a distinct name, making it easy to identify.
The catch block logs the error type and message, providing clear context for debugging.
Introduction
In JavaScript, callbacks are functions passed as arguments to other functions, enabling asynchronous operations. However, they can lead to complex and hard-to-manage code, often referred to as "callback hell."
Promise Basics
Promises offer a cleaner alternative for handling asynchronous operations. A promise represents a value that may be available now, or in the future, or never. It can be in one of three states: pending, fulfilled, or rejected. For example:
let myPromise = new Promise((resolve, reject) => {
// Simulate an async operation
setTimeout(() => {
resolve("Operation successful!");
}, 1000);
});
Promise Chaining
Promises can be chained using the .then() method, allowing for sequential execution of asynchronous tasks. Each .then() returns a new promise:
myPromise
.then(result => {
console.log(result);
return "Next step!";
})
.then(nextResult => console.log(nextResult));
Error Handling in Promises
Errors in promises can be caught using the .catch() method, which handles any rejection in the promise chain:
myPromise
.then(result => {
throw new Error("Something went wrong!");
})
.catch(error => console.error("Error:", error.message));
Promise API
JavaScript provides a built-in Promise API that includes methods like Promise.all() for handling multiple promises simultaneously and Promise.race() for resolving as soon as one of the promises resolves or rejects.
Promisify
You can convert callback-based functions into promises using a technique called "promisification," which allows for cleaner asynchronous code. For example:
const fs = require('fs');
const readFile = (filePath) => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
Microtasks
Promises are executed in the microtask queue, allowing them to complete before the next event loop iteration. This ensures that promise resolutions are processed promptly.
Async/Await
Introduced in ES2017, async/await provides a more synchronous way to write asynchronous code. An async function returns a promise, and the await keyword pauses execution until the promise is resolved:
async function fetchData() {
try {
const data = await readFile('example.txt');
console.log(data);
} catch (error) {
console.error("Error:", error.message);
}
}
What is a Module?
In JavaScript, a module is essentially a single file that encapsulates code, allowing it to be organized and reused efficiently. Modules can interact with one another using the export and import keywords, facilitating the sharing of functionality across different parts of an application.
Importance of Modularity
Modularity is crucial in large-scale software development as it breaks down applications into manageable, interchangeable components. This approach enhances maintainability and scalability, making it easier to develop and update code.
Exporting and Importing
The export keyword is used to specify which variables and functions should be accessible from outside the module. For example:
// mathModule.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
To utilize these exported functions in another module, you can use the import statement:
// main.js
import { add, subtract } from './mathModule.js';
console.log(add(5, 3)); // Outputs: 8
console.log(subtract(5, 3)); // Outputs: 2
What is Importing?
Importing in JavaScript is the process of bringing in exported code from one module to another. This functionality is vital for integrating various components and libraries, enabling developers to build complex applications efficiently.
Importing Syntax
One common way to import code is using the syntax import * as <varName>. This method imports all exported elements from a module and assigns them to a single object, making it easier to access multiple exports without needing to import each one individually.
Example of Importing All Exports
Consider a module named utils.js that exports several utility functions:
// utils.js
export const formatDate = (date) => date.toISOString();
export const parseDate = (dateString) => new Date(dateString);
You can import all of these functions into another file like this:
// main.js
import * as utils from './utils.js';
const date = new Date();
console.log(utils.formatDate(date)); // Outputs the date in ISO format
console.log(utils.parseDate('2022-01-01')); // Converts string to Date object
Curly Braces for Specific Imports
In cases where you only want to import specific elements, you can use curly braces to list them explicitly:
import { formatDate } from './utils.js';
console.log(formatDate(date)); // Outputs the date in ISO format
What are Dynamic Imports?
Dynamic imports provide a flexible method for loading modules in JavaScript, contrasting sharply with traditional static imports. While static imports require all modules to be loaded at the start of a script, potentially leading to longer initial load times, dynamic imports allow modules to be loaded on demand. This approach can significantly improve performance and enhance the user experience.
Static VS. Dynamic Imports
In a static import, modules are loaded upfront:
// Static import (traditional method)
import { module } from './path/to/module.js';
In contrast, a dynamic import uses a promise-based syntax, enabling modules to be loaded as needed:
// Dynamic import
const module = await import('./path/to/module.js');
Benefits of Dynamic Imports
Dynamic imports can be particularly useful in scenarios where certain parts of an application are conditionally required or not immediately necessary. This capability allows developers to implement code splitting, which reduces the initial bundle size and improves load times.
Example Use Case
For example, if you have a feature that’s only used in specific conditions (like a settings page), you can dynamically import it when the user navigates to that page:
async function loadSettings() {
const settingsModule = await import('./settings.js');
settingsModule.initialize();
}
I’m Ikoh Sylva, a passionate cloud computing enthusiast with hands-on experience in AWS. I’m documenting my cloud journey from a beginner’s perspective, aiming to inspire others along the way.
If you find my content helpful, please like and follow my posts, and consider sharing this article with anyone starting their own cloud journey.
Let’s connect on social media. I’d love to engage and exchange ideas with you!
Subscribe to my newsletter
Read articles from Ikoh Sylva directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ikoh Sylva
Ikoh Sylva
I'm a Mobile and African Tech Enthusiast with a large focus on Cloud Technology (AWS)