0%
Modern JavaScript: Object-Oriented Programming

OOP

Modern Classes and Objects

10-15 min

Modern JavaScript: Object-Oriented Programming

JavaScript is a flexible language that supports several programming paradigms, including object-oriented programming (OOP). Since ES6, JavaScript has introduced a more intuitive class syntax, even though the underlying model is still based on prototypes. In this tutorial, we will explore OOP in modern JavaScript, including classes, inheritance, mixins, and commonly used design patterns.

The Object Model in JavaScript

Before exploring modern class syntax, it is important to understand how the object model works in JavaScript.

Objects and Prototypes

In JavaScript, almost everything is an object, and inheritance works through prototypes:

// Création d'un objet littéral
const personne = {
    nom: "Alice",
    saluer() {
        return `Bonjour, je suis ${this.nom}`;
    }
};

console.log(personne.saluer()); // "Bonjour, je suis Alice"

// Création d'un nouvel objet basé sur le prototype de personne
const etudiant = Object.create(personne);
etudiant.nom = "Bob";
etudiant.niveau = "Master";

console.log(etudiant.saluer()); // "Bonjour, je suis Bob"
console.log(etudiant.niveau); // "Master"

Constructor Functions (the Old Way)

Before ES6, creating similar objects was usually done with constructor functions:

// Fonction constructeur
function Personne(nom, age) {
    this.nom = nom;
    this.age = age;
}

// Méthode ajoutée au prototype
Personne.prototype.saluer = function() {
    return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
};

// Création d'instances
const alice = new Personne("Alice", 30);
const bob = new Personne("Bob", 25);

console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"
console.log(bob.saluer()); // "Bonjour, je suis Bob et j'ai 25 ans"

Classes in Modern JavaScript (ES6+)

ES6 introduced a more intuitive class syntax that simplifies object creation and the implementation of inheritance.

Class Definition

class Personne {
    // Constructeur
    constructor(nom, age) {
        this.nom = nom;
        this.age = age;
    }
    
    // Méthodes
    saluer() {
        return `Bonjour, je suis ${this.nom} et j'ai ${this.age} ans`;
    }
    
    decrire() {
        return `${this.nom} est une personne de ${this.age} ans`;
    }
}

// Création d'instances
const alice = new Personne("Alice", 30);
console.log(alice.saluer()); // "Bonjour, je suis Alice et j'ai 30 ans"

Static Properties and Methods

Static methods and properties belong to the class itself, not to its instances:

class MathUtils {
    // Propriété statique (ES2022+)
    static PI = 3.14159;
    
    // Méthode statique
    static carre(x) {
        return x * x;
    }
    
    static aire_cercle(rayon) {
        return MathUtils.PI * MathUtils.carre(rayon);
    }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.carre(4)); // 16
console.log(MathUtils.aire_cercle(2)); // 12.56636

// Les propriétés et méthodes statiques ne sont pas accessibles sur les instances
const utils = new MathUtils();
// console.log(utils.PI); // undefined

Accessors (Getters and Setters)

Accessors let you control access to a class’s properties:

class Compte {
    constructor(proprietaire) {
        this.proprietaire = proprietaire;
        this._solde = 0; // Convention : _ indique une propriété "privée"
    }
    
    // Getter
    get solde() {
        return `${this._solde} €`;
    }
    
    // Setter
    set solde(valeur) {
        if (isNaN(valeur)) {
            throw new Error("Le solde doit être un nombre");
        }
        this._solde = valeur;
    }
    
    // Méthodes normales
    deposer(montant) {
        this.solde = this._solde + montant;
        return this._solde;
    }
    
    retirer(montant) {
        if (montant > this._solde) {
            throw new Error("Fonds insuffisants");
        }
        this.solde = this._solde - montant;
        return this._solde;
    }
}

const compte = new Compte("Alice");
compte.deposer(1000);
console.log(compte.solde); // "1000 €"

compte.retirer(500);
console.log(compte.solde); // "500 €"

// compte.solde = "beaucoup"; // Erreur: Le solde doit être un nombre

Class Fields

Class fields, introduced in more recent versions of JavaScript, let you declare properties directly in the class body:

class Produit {
    // Champs publics
    nom;
    prix;
    
    // Champ privé (ECMAScript 2022+)
    #stock = 0;
    
    // Champ statique
    static nbProduits = 0;
    
    constructor(nom, prix, stock) {
        this.nom = nom;
        this.prix = prix;
        this.#stock = stock;
        Produit.nbProduits++;
    }
    
    get stock() {
        return this.#stock;
    }
    
    set stock(valeur) {
        if (valeur < 0) {
            throw new Error("Le stock ne peut pas être négatif");
        }
        this.#stock = valeur;
    }
    
    vendre(quantite) {
        if (quantite > this.#stock) {
            throw new Error("Stock insuffisant");
        }
        this.#stock -= quantite;
        return `${quantite} unité(s) de ${this.nom} vendue(s)`;
    }
    
    // Méthode privée (ECMAScript 2022+)
    #miseAJourInterne() {
        console.log("Mise à jour interne");
    }
}

const p1 = new Produit("Téléphone", 499, 10);
console.log(p1.nom); // "Téléphone"
console.log(p1.stock); // 10

p1.stock = 15;
console.log(p1.stock); // 15

console.log(p1.vendre(3)); // "3 unité(s) de Téléphone vendue(s)"
console.log(p1.stock); // 12

// console.log(p1.#stock); // Erreur: propriété privée
// p1.#miseAJourInterne(); // Erreur: méthode privée

console.log(Produit.nbProduits); // 1

Inheritance and Polymorphism

Inheritance allows a class to inherit the properties and methods of another class.

Inheritance with extends

class Animal {
    constructor(nom) {
        this.nom = nom;
    }
    
    parler() {
        return `${this.nom} fait du bruit`;
    }
}

class Chien extends Animal {
    constructor(nom, race) {
        super(nom); // Appel du constructeur parent
        this.race = race;
    }
    
    // Surcharge de la méthode parler
    parler() {
        return `${this.nom} aboie`;
    }
    
    decrire() {
        return `${this.nom} est un chien de race ${this.race}`;
    }
}

const animal = new Animal("Animal inconnu");
console.log(animal.parler()); // "Animal inconnu fait du bruit"

const rex = new Chien("Rex", "Berger Allemand");
console.log(rex.parler()); // "Rex aboie"
console.log(rex.decrire()); // "Rex est un chien de race Berger Allemand"

The Prototype Chain

Even with modern class syntax, JavaScript still uses prototypes under the hood:

const rex = new Chien("Rex", "Berger Allemand");

console.log(rex instanceof Chien); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true

console.log(Object.getPrototypeOf(rex) === Chien.prototype); // true
console.log(Object.getPrototypeOf(Chien.prototype) === Animal.prototype); // true

Type Checking

To check whether an object is an instance of a class, use the instanceof operator:

function traiterAnimal(animal) {
    if (animal instanceof Chien) {
        console.log(animal.parler()); // Méthode de Chien
    } else if (animal instanceof Animal) {
        console.log(animal.parler()); // Méthode d'Animal
    } else {
        console.log("Ceci n'est pas un animal");
    }
}

traiterAnimal(new Chien("Rex", "Berger Allemand")); // "Rex aboie"
traiterAnimal(new Animal("Lion")); // "Lion fait du bruit"
traiterAnimal({}); // "Ceci n'est pas un animal"

Composition and Mixins

Sometimes inheritance alone is not enough to model complex relationships. Composition and mixins offer flexible alternatives.

Composition

Composition consists of combining simpler objects to create more complex ones:

// Objets de capacités
const peutNager = {
    nager() {
        return `${this.nom} nage`;
    }
};

const peutVoler = {
    voler() {
        return `${this.nom} vole`;
    }
};

// Classe de base
class Animal {
    constructor(nom) {
        this.nom = nom;
    }
    
    manger() {
        return `${this.nom} mange`;
    }
}

// Composition par mixin
class Canard extends Animal {
    constructor(nom) {
        super(nom);
        // Composition: ajouter des capacités
        Object.assign(this, peutNager, peutVoler);
    }
}

class Poisson extends Animal {
    constructor(nom) {
        super(nom);
        // Composition: seulement nager
        Object.assign(this, peutNager);
    }
}

const donald = new Canard("Donald");
console.log(donald.manger()); // "Donald mange"
console.log(donald.nager()); // "Donald nage"
console.log(donald.voler()); // "Donald vole"

const nemo = new Poisson("Nemo");
console.log(nemo.nager()); // "Nemo nage"
// console.log(nemo.voler()); // Erreur: nemo.voler n'est pas une fonction

Functional Mixins

Functional mixins are functions that add properties and methods to an existing object:

// Mixin fonctionnel
function avecTimestamp(Base) {
    return class extends Base {
        constructor(...args) {
            super(...args);
            this.createdAt = new Date();
        }
        
        getAge() {
            return new Date() - this.createdAt;
        }
    };
}

function avecLogging(Base) {
    return class extends Base {
        log(message) {
            console.log(`[${this.constructor.name}] ${message}`);
        }
    };
}

// Application des mixins
class Utilisateur {
    constructor(nom) {
        this.nom = nom;
    }
}

// Combinaison de mixins
const UtilisateurAvecExtensions = avecLogging(avecTimestamp(Utilisateur));

const user = new UtilisateurAvecExtensions("Alice");
console.log(user.nom); // "Alice"
console.log(user.createdAt); // Date de création
user.log("Instance créée"); // "[UtilisateurAvecExtensions] Instance créée"

Design Patterns in JavaScript

Design patterns are reusable solutions to recurring problems. Here are some common patterns in JavaScript.

Singleton

The Singleton pattern ensures that a class has only one instance and provides a global access point to that instance:

class Configuration {
    constructor() {
        if (Configuration._instance) {
            return Configuration._instance;
        }
        
        this.theme = "clair";
        this.langue = "fr";
        this.notifications = true;
        
        Configuration._instance = this;
    }
    
    static getInstance() {
        return new Configuration();
    }
}

const config1 = Configuration.getInstance();
const config2 = Configuration.getInstance();

console.log(config1 === config2); // true

config1.theme = "sombre";
console.log(config2.theme); // "sombre" (même instance)

Factory

The Factory pattern creates objects without exposing the creation logic:

// Classes concrètes
class VoitureEconomique {
    constructor(options) {
        this.portes = options.portes || 4;
        this.couleur = options.couleur || "blanche";
        this.consommation = "faible";
    }
}

class VoitureLuxe {
    constructor(options) {
        this.portes = options.portes || 2;
        this.couleur = options.couleur || "noire";
        this.consommation = "élevée";
        this.options = ["cuir", "GPS", "toit ouvrant"];
    }
}

class VoitureSport {
    constructor(options) {
        this.portes = 2;
        this.couleur = options.couleur || "rouge";
        this.consommation = "très élevée";
        this.vitesseMax = 300;
    }
}

// Factory
class VoitureFactory {
    createVoiture(type, options) {
        switch(type) {
            case "economique":
                return new VoitureEconomique(options);
            case "luxe":
                return new VoitureLuxe(options);
            case "sport":
                return new VoitureSport(options);
            default:
                throw new Error(`Type de voiture non reconnu: ${type}`);
        }
    }
}

// Utilisation
const factory = new VoitureFactory();
const maVoiture = factory.createVoiture("luxe", { couleur: "bleu", portes: 4 });
console.log(maVoiture); // VoitureLuxe avec options personnalisées

Observer

The Observer pattern lets an object notify other objects of state changes:

class Sujet {
    constructor() {
        this.observateurs = [];
    }
    
    souscrire(observateur) {
        this.observateurs.push(observateur);
    }
    
    desinscrire(observateur) {
        this.observateurs = this.observateurs.filter(obs => obs !== observateur);
    }
    
    notifier(donnees) {
        this.observateurs.forEach(observateur => observateur.update(donnees));
    }
}

class Observable extends Sujet {
    constructor() {
        super();
        this.etat = null;
    }
    
    getEtat() {
        return this.etat;
    }
    
    setEtat(etat) {
        this.etat = etat;
        this.notifier(this);
    }
}

// Exemple d'utilisation: système de notification météo
class StationMeteo extends Observable {
    setMesures(temperature, humidite, pression) {
        this.etat = { temperature, humidite, pression, date: new Date() };
        this.notifier(this);
    }
}

// Observateurs
class AffichageConditionsActuelles {
    update(station) {
        const { temperature, humidite } = station.getEtat();
        console.log(`Conditions actuelles: ${temperature}°C et ${humidite}% d'humidité`);
    }
}

class AffichageStatistiques {
    constructor() {
        this.temperatures = [];
    }
    
    update(station) {
        const { temperature } = station.getEtat();
        this.temperatures.push(temperature);
        
        const max = Math.max(...this.temperatures);
        const min = Math.min(...this.temperatures);
        const moy = this.temperatures.reduce((acc, val) => acc + val, 0) / this.temperatures.length;
        
        console.log(`Statistiques - Max: ${max}°C, Min: ${min}°C, Moyenne: ${moy.toFixed(1)}°C`);
    }
}

// Utilisation
const station = new StationMeteo();
const affichageConditions = new AffichageConditionsActuelles();
const affichageStats = new AffichageStatistiques();

station.souscrire(affichageConditions);
station.souscrire(affichageStats);

// Simule des changements météo
station.setMesures(20, 65, 1013);
station.setMesures(22, 70, 1014);
station.setMesures(19, 75, 1012);

Module

The Module pattern encapsulates functionality and exposes a public API while keeping certain details private:

// Module with IIFE (old style)
const Compteur = (function() {
    // Private variables
    let compte = 0;
    
    // Private methods
    function valider(valeur) {
        return typeof valeur === 'number' && !isNaN(valeur);
    }
    
    // Public API
    return {
        incrementer() {
            return ++compte;
        },
        decrementer() {
            return --compte;
        },
        ajouter(valeur) {
            if (!valider(valeur)) {
                throw new Error("Valeur invalide");
            }
            compte += valeur;
            return compte;
        },
        getCompte() {
            return compte;
        }
    };
})();

console.log(Compteur.getCompte()); // 0
Compteur.incrementer();
Compteur.incrementer();
console.log(Compteur.getCompte()); // 2
Compteur.ajouter(5);
console.log(Compteur.getCompte()); // 7
// console.log(Compteur.compte); // undefined (private)

Decorator

The Decorator pattern lets you add behaviors to individual objects without affecting the behavior of other objects of the same class:

// Base component
class Cafe {
    cout() {
        return 2.5;
    }
    
    description() {
        return "Café simple";
    }
}

// Decorators
class LaitDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.5;
    }
    
    description() {
        return `${this.cafe.description()} avec du lait`;
    }
}

class SucreDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.2;
    }
    
    description() {
        return `${this.cafe.description()} avec du sucre`;
    }
}

class CannelleDecorator {
    constructor(cafe) {
        this.cafe = cafe;
    }
    
    cout() {
        return this.cafe.cout() + 0.3;
    }
    
    description() {
        return `${this.cafe.description()} avec de la cannelle`;
    }
}

// Usage
let monCafe = new Cafe();
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple : 2.5€"

// Adding milk
monCafe = new LaitDecorator(monCafe);
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple avec du lait : 3€"

// Adding sugar
monCafe = new SucreDecorator(monCafe);
console.log(`${monCafe.description()} : ${monCafe.cout()}€`);
// "Café simple avec du lait avec du sucre : 3.2€"

// Creating a custom coffee directly
const cafeComplet = new CannelleDecorator(new SucreDecorator(new LaitDecorator(new Cafe())));
console.log(`${cafeComplet.description()} : ${cafeComplet.cout()}€`);
// "Café simple avec du lait avec du sucre avec de la cannelle : 3.5€"

Advanced Object Management

JavaScript offers several advanced ways to manipulate and control objects.

Object.defineProperty

Object.defineProperty lets you define properties with precise control over their behavior:

const personne = {};

Object.defineProperty(personne, 'nom', {
    value: 'Alice',
    writable: true,      // can be modified
    enumerable: true,    // shows up in for...in loops
    configurable: true   // can be deleted
});

Object.defineProperty(personne, 'age', {
    value: 30,
    writable: false      // cannot be modified
});

console.log(personne.nom); // "Alice"
personne.nom = "Bob";
console.log(personne.nom); // "Bob"

console.log(personne.age); // 30
personne.age = 40;
console.log(personne.age); // 30 (unchanged because writable: false)

Proxy

Proxies let you customize the behavior of fundamental operations on objects:

const cible = {
    message: "Hello",
    prix: 100
};

const handler = {
    // Intercepting property access
    get(cible, prop, receiver) {
        console.log(`Accès à la propriété: ${prop}`);
        
        if (prop === 'prix') {
            return `${cible[prop]}€`;
        }
        
        return cible[prop];
    },
    
    // Intercepting property modification
    set(cible, prop, valeur, receiver) {
        console.log(`Modification de ${prop}: ${valeur}`);
        
        if (prop === 'prix' && valeur < 0) {
            throw new Error("Le prix ne peut pas être négatif");
        }
        
        cible[prop] = valeur;
        return true; // success
    }
};

const proxy = new Proxy(cible, handler);

console.log(proxy.message); // Log: "Accès à la propriété: message" then "Hello"
console.log(proxy.prix); // Log: "Accès à la propriété: prix" then "100€"

proxy.message = "Bonjour"; // Log: "Modification de message: Bonjour"
// proxy.prix = -50; // Error: Le prix ne peut pas être négatif

Symbols and Well-Known Properties

Symbols can be used as unique, non-enumerable property keys:

// Creating a symbol
const idSymbol = Symbol("id");

const utilisateur = {
    nom: "Alice",
    [idSymbol]: 12345  // symbol-based property
};

console.log(utilisateur.nom); // "Alice"
console.log(utilisateur[idSymbol]); // 12345

// Symbol-based properties are not enumerable by default
for (let key in utilisateur) {
    console.log(key); // Only prints "nom", not the symbol
}

// Well-known symbols to customize behavior
const collectionPersonnalisee = {
    elements: [1, 2, 3, 4, 5],
    
    // Customizing iteration
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.elements.length) {
                    return { value: this.elements[index++] * 2, done: false };
                }
                return { done: true };
            }
        };
    }
};

// Usage with for...of, which uses the iterator
for (const item of collectionPersonnalisee) {
    console.log(item); // 2, 4, 6, 8, 10
}

Practice Exercises

To solidify your knowledge, try these exercises:

  1. Shape system: Create a class hierarchy with a Forme class as the base and subclasses such as Cercle, Rectangle, and Triangle, each with its own methods for calculating area and perimeter.

  2. Library management: Implement a library management system with classes for Livre, Auteur, Bibliothèque, and Emprunteur. Use encapsulation to protect data and mixins to add features such as loan tracking.

  3. Mini UI framework: Create a small UI framework with a Component base class and specific components such as Button, Input, and Panel that inherit from it. Use methods such as render() and update().

  4. Pattern implementation: Implement the Singleton pattern for a logging service and the Factory pattern to create different types of notifications (email, SMS, push).

  5. Validation API: Create a validation API with decorators to validate an object’s properties (for example, minimum string length or value range for a number).

Complete Example: Product Management System

Here is an example that combines several concepts covered in this tutorial:

// Base class
class Produit {
    #id;
    #nom;
    #prix;
    #stock;
    static #compteur = 0;
    
    constructor(nom, prix, stock = 0) {
        this.#id = ++Produit.#compteur;
        this.#nom = nom;
        this.#prix = prix;
        this.#stock = stock;
    }
    
    // Getters/Setters
    get id() { return this.#id; }
    
    get nom() { return this.#nom; }
    set nom(value) { this.#nom = value; }
    
    get prix() { return this.#prix; }
    set prix(value) {
        if (value < 0) throw new Error("Le prix ne peut pas être négatif");
        this.#prix = value;
    }
    
    get stock() { return this.#stock; }
    set stock(value) {
        if (value < 0) throw new Error("Le stock ne peut pas être négatif");
        this.#stock = value;
    }
    
    // Methods
    ajouter_stock(quantite) {
        this.stock += quantite;
        return this.stock;
    }
    
    retirer_stock(quantite) {
        if (quantite > this.stock) throw new Error("Stock insuffisant");
        this.stock -= quantite;
        return this.stock;
    }
    
    toString() {
        return `Produit #${this.id}: ${this.nom} - Prix: ${this.prix}€ - Stock: ${this.stock}`;
    }
}

// Specialized subclass
class ProduitPerissable extends Produit {
    #dateExpiration;
    
    constructor(nom, prix, stock, dateExpiration) {
        super(nom, prix, stock);
        this.#dateExpiration = new Date(dateExpiration);
    }
    
    get dateExpiration() { return this.#dateExpiration; }
    
    estPerime() {
        return new Date() > this.#dateExpiration;
    }
    
    toString() {
        return `${super.toString()} - Expire le: ${this.#dateExpiration.toLocaleDateString()}`;
    }
}

// Mixin for tracking changes
const avecHistorique = Base => class extends Base {
    #historique = [];
    
    constructor(...args) {
        super(...args);
    }
    
    ajouterEvenement(type, details) {
        const evenement = {
            type,
            details,
            date: new Date()
        };
        this.#historique.push(evenement);
        return evenement;
    }
    
    get historique() {
        return [...this.#historique];
    }
};

// Applying the mixin
class ProduitAvecHistorique extends avecHistorique(Produit) {
    constructor(...args) {
        super(...args);
        this.ajouterEvenement("creation", { prix: args[1], stock: args[2] || 0 });
    }
    
    // Override to add events
    ajouter_stock(quantite) {
        const stockAvant = this.stock;
        const nouveauStock = super.ajouter_stock(quantite);
        this.ajouterEvenement("ajout_stock", { 
            avant: stockAvant,
            ajout: quantite,
            apres: nouveauStock
        });
        return nouveauStock;
    }
    
    retirer_stock(quantite) {
        const stockAvant = this.stock;
        const nouveauStock = super.retirer_stock(quantite);
        this.ajouterEvenement("retrait_stock", { 
            avant: stockAvant,
            retrait: quantite,
            apres: nouveauStock
        });
        return nouveauStock;
    }
    
    set prix(value) {
        const ancienPrix = this.prix;
        super.prix = value;
        this.ajouterEvenement("changement_prix", { 
            avant: ancienPrix,
            apres: value
        });
    }
}

// Factory for creating different types of products
class ProduitFactory {
    creerProduit(type, ...args) {
        switch(type) {
            case "standard":
                return new ProduitAvecHistorique(...args);
            case "perissable":
                return new ProduitPerissable(...args);
            default:
                throw new Error(`Type de produit inconnu: ${type}`);
        }
    }
}

// Catalog manager (Singleton)
class CatalogueGestionnaire {
    #produits = new Map();
    static #instance;
    
    constructor() {
        if (CatalogueGestionnaire.#instance) {
            return CatalogueGestionnaire.#instance;
        }
        CatalogueGestionnaire.#instance = this;
    }
    
    ajouterProduit(produit) {
        this.#produits.set(produit.id, produit);
        return produit;
    }
    
    getProduit(id) {
        return this.#produits.get(id);
    }
    
    listerProduits() {
        return [...this.#produits.values()];
    }
    
    listerProduitsEnStock() {
        return this.listerProduits().filter(p => p.stock > 0);
    }
}

// Demonstration
function demoProduits() {
    // Creating products with the Factory
    const factory = new ProduitFactory();
    const p1 = factory.creerProduit("standard", "Clavier", 49.99, 10);
    const p2 = factory.creerProduit("standard", "Souris", 29.99, 15);
    const p3 = factory.creerProduit("perissable", "Yaourt", 2.99, 50, "2023-12-31");
    
    // Using the singleton
    const catalogue = new CatalogueGestionnaire();
    catalogue.ajouterProduit(p1);
    catalogue.ajouterProduit(p2);
    catalogue.ajouterProduit(p3);
    
    // Operations
    console.log("Produits en catalogue:");
    catalogue.listerProduits().forEach(p => console.log(` - ${p}`));
    
    p1.ajouter_stock(5);
    p2.retirer_stock(3);
    p1.prix = 45.99;
    
    console.log("\nHistorique du clavier:");
    p1.historique.forEach((e, i) => {
        console.log(`${i+1}. [${e.date.toLocaleTimeString()}] ${e.type}: `, e.details);
    });
    
    console.log("\nTest d'un produit périssable:");
    console.log(p3.toString());
    console.log(`Est périmé: ${p3.estPerime()}`);
}

// Execution
demoProduits();

Conclusion

In this tutorial, we explored object-oriented programming in modern JavaScript:

  • Modern class syntax and its features (constructors, getters/setters, static methods, private fields)
  • Inheritance and polymorphism through extends and method overriding
  • Composition and mixins for flexible code reuse
  • Common design patterns such as Singleton, Factory, Observer, and Decorator
  • Advanced object manipulation techniques with Object.defineProperty and Proxy

JavaScript offers a flexible approach to OOP that combines elements of prototype-based programming with a more familiar syntax inspired by classic object-oriented languages. This flexibility lets you choose the style that best fits your needs, whether that’s classical inheritance, composition, or a more functional approach.

In the next tutorial, we’ll explore Promises and async/await for handling asynchronous code.


Need to review destructuring? Check out our previous tutorial on destructuring.

Ready to continue? Move on to the next tutorial on Promises and async/await.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!