Clonage profond d'objets
Le clonage profond nécessite la gestion des références circulaires et des types de données complexes. L'utilisation d'un WeakMap permet d'éviter les fuites de mémoire tout en conservant une trace des objets déjà copiés. L'emploi de Reflect.ownKeys assure que les propriétés non énumérables et les symboles sont également dupliqués.
function copierProfondement(objetSource, memoire = new WeakMap()) {
if (objetSource === null || typeof objetSource !== 'object') {
return objetSource;
}
if (memoire.has(objetSource)) {
return memoire.get(objetSource);
}
const copie = Array.isArray(objetSource) ? [] : Object.create(Object.getPrototypeOf(objetSource));
memoire.set(objetSource, copie);
for (const propriete of Reflect.ownKeys(objetSource)) {
copie[propriete] = copierProfondement(objetSource[propriete], memoire);
}
return copie;
}
Orchestration de promesses : All et Race
Recréer les méthodes statiques de l'objet Promise permet de comprendre en profondeur le fonctionnement de l'asynchronisme. Promise.all attend la résolution de toutes les promesses et préserve l'ordre des résultats, tandis que Promise.race se base sur la première promesse à changer d'état, qu'elle soit résolue ou rejetée.
function tousLesPromesses(listePromesses) {
return new Promise((resoudre, rejeter) => {
const resultatFinal = [];
let promessesRestantes = listePromesses.length;
if (promessesRestantes === 0) {
resoudre(resultatFinal);
return;
}
listePromesses.forEach((promesse, position) => {
Promise.resolve(promesse)
.then(valeur => {
resultatFinal[position] = valeur;
promessesRestantes -= 1;
if (promessesRestantes === 0) {
resoudre(resultatFinal);
}
})
.catch(erreur => rejeter(erreur));
});
});
}
function courseDePromesses(iterablePromesses) {
return new Promise((valider, invalider) => {
for (const tache of iterablePromesses) {
Promise.resolve(tache)
.then(donnee => valider(donnee))
.catch(probleme => invalider(probleme));
}
});
}
Aplanissement de tableaux multidimensionnels
Transformer une structure de données imbriquée en un tableau à une seule dimension peut être réalisé de manière élégante avec la méthode reduce. Cette approche permet également de contrôler la profondeur de récursion souhaitée.
function aplatirTableau(tableauImbrique, profondeur = Infinity) {
return tableauImbrique.reduce((accumulateur, element) => {
if (Array.isArray(element) && profondeur > 0) {
accumulateur.push(...aplatirTableau(element, profondeur - 1));
} else {
accumulateur.push(element);
}
return accumulateur;
}, []);
}
Currying de fonctions
Le currying transforme une fonction acceptant plusieurs arguments en une séquence de fonctions. Cela facilite la composition fonctionnelle, la réutilisabilité du code et la création de fonctions partielles en vérifiant dynamiquement la longueur des paramètres attendus.
function transformerEnCurry(fonctionCible) {
return function executeur(...parametresInitiaux) {
if (parametresInitiaux.length >= fonctionCible.length) {
return fonctionCible.apply(this, parametresInitiaux);
}
return function(...parametresSuivants) {
return executeur.apply(this, [...parametresInitiaux, ...parametresSuivants]);
};
};
}
Simulation des méthodes Call et Bind
Réimplémenter les méthodes du prototype Function exige une manipulation minutieuse du contexte d'exécution. L'utilisation de Symbol garantit l'absence de collision avec les propriétés existantes de l'objet contexte, et Reflect.deleteProperty assure un nettoyage sécurisé.
Function.prototype.appeler = function(contexte, ...argumentsFonction) {
const contexteCible = contexte || globalThis;
const clefUnique = Symbol('methodeTemporaire');
contexteCible[clefUnique] = this;
const resultatExecution = contexteCible[clefUnique](...argumentsFonction);
Reflect.deleteProperty(contexteCible, clefUnique);
return resultatExecution;
};
Function.prototype.lier = function(contexteLie, ...argsInitiaux) {
const fonctionOriginale = this;
return function fonctionLiee(...argsTardifs) {
return fonctionOriginale.apply(contexteLie, [...argsInitiaux, ...argsTardifs]);
};
};
Simulation de l'opérateur New
L'opérateur new lie le prototype, exécute la fonction constructeur avec le nouveau contexte, et retourne soit l'objet instancié, soit une valeur explicite si le constructeur renvoie un objet ou une fonction.
function instancier(Constructeur, ...paramsConstruction) {
const nouvelleInstance = Object.create(Constructeur.prototype);
const resultatInit = Constructeur.apply(nouvelleInstance, paramsConstruction);
const estObjet = resultatInit !== null && (typeof resultatInit === 'object' || typeof resultatInit === 'function');
return estObjet ? resultatInit : nouvelleInstance;
}
Conversion d'un tableau plat en structure arborescente
Convertir une liste plate avec des identifiants parents en une hiérarchie d'arbres est une tâche courante dans le traitement des données. L'approche par dictionnaire (Map) optimise cette opération en réduisant la complexité temporelle à O(N), évitant ainsi les filtrages récursifs coûteux.
function convertirEnArborescence(elements, idParentRacine = null) {
const cacheElements = new Map();
const racines = [];
elements.forEach(element => {
cacheElements.set(element.id, { ...element, enfants: [] });
});
cacheElements.forEach(noeud => {
if (noeud.parentId === idParentRacine) {
racines.push(noeud);
} else {
const parent = cacheElements.get(noeud.parentId);
if (parent) {
parent.enfants.push(noeud);
}
}
});
return racines;
}
Anti-rebond (Debounce) et Limitation de débit (Throttle)
Ces techniques sont essentielles pour optimiser les performances des événements fréquents tels que le redimensionnement de la fenêtre ou le défilement. L'anti-rebond attend une période d'inactivité avant d'exécuter l'action, tandis que la limitation de débit garantit une exécution régulière à intervalles fixes.
function antiRebond(action, delai = 300) {
let minuteur = null;
return function(...donnees) {
if (minuteur) clearTimeout(minuteur);
minuteur = setTimeout(() => {
action.apply(this, donnees);
}, delai);
};
}
function limiterDebit(action, intervalle = 300) {
let enAttente = false;
return function(...donnees) {
if (enAttente) return;
enAttente = true;
action.apply(this, donnees);
setTimeout(() => {
enAttente = false;
}, intervalle);
};
}
Contrôle de requêtes concurrentes
Limiter le nombre de requêtes réseau simultanées évite de surcharger le serveur ou d'épuiser les ressources du navigateur. Un système de file d'attente avec un pool de connexions permet de gérer un grand volume de tâches asynchrones de manière contrôlée et efficace.
function requetesSimultanees(listeRequetes, limite) {
return new Promise((resoudre, rejeter) => {
const reponses = new Array(listeRequetes.length);
let indexCourant = 0;
let terminees = 0;
const lancerSuivante = () => {
if (indexCourant >= listeRequetes.length) return;
const position = indexCourant++;
Promise.resolve(listeRequetes[position]())
.then(donnee => {
reponses[position] = donnee;
terminees++;
if (terminees === listeRequetes.length) {
resoudre(reponses);
} else {
lancerSuivante();
}
})
.catch(rejeter);
};
for (let i = 0; i < Math.min(limite, listeRequetes.length); i++) {
lancerSuivante();
}
});
}