AltSchool Of Engineering Tinyuka’24 Month 5 Week 2
Ikoh Sylva

Ikoh Sylva @ikoh_sylva

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

Location:
Lagos, Nigeria.
Joined:
Jul 24, 2019

AltSchool Of Engineering Tinyuka’24 Month 5 Week 2

Publish Date: Jul 12
3 0

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!

Image of a workstation

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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!`);
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Try Block: Code that may cause an error is placed inside the try block.
  2. Catch Block: If an error occurs, control is transferred to the catch block, where you can handle the error gracefully.
  3. 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.");
}
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Image of a workstation

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);
        });
    });
};
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 . 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Image of a workstation

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!

LinkedIn Facebook X

Comments 0 total

    Add comment