Copie superficielle de base
Pour comprendre la mécanique du clonage profond, il est essentiel de partir d'une copie superficielle. Cette approche ne duplique que le premier niveau de propriétés de l'objet source.
function copieNiveauUn(source) {
const cible = {};
for (const cle in source) {
cible[cle] = source[cle];
}
return cible;
}
const donneesSource = {
identifiant: "user_01",
profil: {
nom: "Dupont",
},
};
const copie = copieNiveauUn(donneesSource);
copie.identifiant = "user_02";
copie.profil.nom = "Martin";
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);
La modification d'une propriété de premier niveau n'affecte pas l'objet source. Cependant, la mutation d'une propriété imbriquée (comme profil.nom) altère également l'objet d'origine, car les références des objets internes sont partagées.
Approche récursive pour les objets imbriqués
Pour isoler complètement les objets internes, une traversal récursif est nécessaire. Lorsque la valeur rencontrée n'est pas un objet, elle est retournée telle quelle.
function verifierObjet(val) {
return val !== null && (typeof val === 'object' || typeof val === 'function');
}
function clonageRecursif(entree) {
if (!verifierObjet(entree)) return entree;
const cible = {};
for (const cle in entree) {
cible[cle] = clonageRecursif(entree[cle]);
}
return cible;
}
const donneesSource = {
identifiant: "user_01",
profil: {
nom: "Dupont",
adresse: {
ville: "Paris",
},
},
competences: ["JavaScript", "Node.js", "React"],
};
const copie = clonageRecursif(donneesSource);
copie.profil.nom = "Martin";
copie.profil.adresse.ville = "Lyon";
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);
Chaque niveau d'imbrication dispose désormais de sa propre allocation mémoire. Toutefois, le tableau competences est traité comme un objet standard, ce qui transforme sa structure en un objet avec des clés numériques.
Distinction entre les tableaux et les objets
Il est crucial de différencier les tableaux des objets purs. Si la source est un tableau, le conteneur de destination doit également être initialisé en tant que tableau.
function clonageAvecTableaux(entree) {
if (!verifierObjet(entree)) return entree;
const cible = Array.isArray(entree) ? [] : {};
for (const cle in entree) {
cible[cle] = clonageAvecTableaux(entree[cle]);
}
return cible;
}
const donneesSource = {
identifiant: "user_01",
profil: {
nom: "Dupont",
adresse: { ville: "Paris" },
},
competences: ["JavaScript", "Node.js"],
executerTache() {
return "Tâche en cours...";
},
};
const copie = clonageAvecTableaux(donneesSource);
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);
Les tableaux sont maintenant correctement instanciés. Cependant, l'ajout d'une méthode (executerTache) révèle un problème : la fonction est clonée comme un objet vide, perdant ainsi son comportement.
Préservation des références de fonctions
Les fonctions étant conçues pour être réutilisées et ne possédant généralement pas d'état interne mutable de la même manière que les objets, il est plus optimal de conserver leur référence originale plutôt que de tenter de les cloner.
function clonageAvecFonctions(entree) {
if (typeof entree === 'function') return entree;
if (!verifierObjet(entree)) return entree;
const cible = Array.isArray(entree) ? [] : {};
for (const cle in entree) {
cible[cle] = clonageAvecFonctions(entree[cle]);
}
return cible;
}
const cleSymbole = Symbol("cleUnique");
const valSymbole = Symbol("valeurUnique");
const donneesSource = {
identifiant: "user_01",
profil: { nom: "Dupont", adresse: { ville: "Paris" } },
competences: ["JavaScript", "Node.js"],
executerTache() { return "Tâche en cours..."; },
[cleSymbole]: "valeur_cle",
metadata: valSymbole,
};
const copie = clonageAvecFonctions(donneesSource);
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);
Les fonctions partagent désormais la même adresse mémoire. Cependant, l'utilisation de Symbol comme clé entraîne la disparition de cete propriété dans l'objet cloné, car la boucle for...in n'itère pas sur les clés symboliques.
Support des clés et valeurs de type Symbol
Pour capturer les propriétés dont les clés sont des symboles, il faut utiliser Object.getOwnPropertySymbols. De plus, si la valeur elle-même est un symbole, une nouvelle instance doit être créée.
function clonageAvecSymbols(entree) {
if (typeof entree === 'symbol') return Symbol(entree.description);
if (typeof entree === 'function') return entree;
if (!verifierObjet(entree)) return entree;
const cible = Array.isArray(entree) ? [] : {};
for (const cle in entree) {
cible[cle] = clonageAvecSymbols(entree[cle]);
}
const clesSymbols = Object.getOwnPropertySymbols(entree);
for (const sym of clesSymbols) {
cible[sym] = clonageAvecSymbols(entree[sym]);
}
return cible;
}
const cleSymbole = Symbol("cleUnique");
const valSymbole = Symbol("valeurUnique");
const ensemble = new Set([1, 2, 3]);
const dictionnaire = new Map([["nom", "Dupont"], ["age", 30]]);
const donneesSource = {
identifiant: "user_01",
profil: { nom: "Dupont", adresse: { ville: "Paris" } },
competences: ["JavaScript", "Node.js"],
executerTache() { return "Tâche en cours..."; },
[cleSymbole]: "valeur_cle",
metadata: valSymbole,
ensemble,
dictionnaire,
};
const copie = clonageAvecSymbols(donneesSource);
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);
Les symboles sont maintenant correctement transférés. Néanmoins, les structures de données natives comme Map et Set sont converties en objets vides, car elles ne sont pas itérables via for...in.
Intégration des structures Map et Set
Pour gérer ces collections, il faut intercepter leurs instances spécifiques et reconstruire une nouvelle structure. Pour garantir un clonage véritablement profond, les éléments internes de ces collections doivent également être clonés récursivement.
function clonageAvecCollections(entree) {
if (entree instanceof Set) {
return new Set([...entree].map(item => clonageAvecCollections(item)));
}
if (entree instanceof Map) {
return new Map([...entree].map(([k, v]) => [
clonageAvecCollections(k),
clonageAvecCollections(v)
]));
}
if (typeof entree === 'symbol') return Symbol(entree.description);
if (typeof entree === 'function') return entree;
if (!verifierObjet(entree)) return entree;
const cible = Array.isArray(entree) ? [] : {};
for (const cle in entree) {
cible[cle] = clonageAvecCollections(entree[cle]);
}
const clesSymbols = Object.getOwnPropertySymbols(entree);
for (const sym of clesSymbols) {
cible[sym] = clonageAvecCollections(entree[sym]);
}
return cible;
}
Résolution des références circulaires
Un problème majeur survient lorsque l'objet source contient des références circulaires (par exemple, donneesSource.self = donneesSource). La récursivité infinie provoque alors un débordement de la pile d'appels (RangeError).
La solution consiste à utiilser un WeakMap comme cache pour mémoriser les objets déjà clonés. Avant de traiter un objet, on vérifie s'il est déjà présent dans le cache.
function cloneProfondComplet(entree, cache = new WeakMap()) {
if (entree instanceof Set) {
return new Set([...entree].map(item => cloneProfondComplet(item, cache)));
}
if (entree instanceof Map) {
return new Map([...entree].map(([k, v]) => [
cloneProfondComplet(k, cache),
cloneProfondComplet(v, cache)
]));
}
if (typeof entree === 'symbol') return Symbol(entree.description);
if (typeof entree === 'function') return entree;
if (!verifierObjet(entree)) return entree;
if (cache.has(entree)) {
return cache.get(entree);
}
const cible = Array.isArray(entree) ? [] : {};
cache.set(entree, cible);
for (const cle in entree) {
cible[cle] = cloneProfondComplet(entree[cle], cache);
}
const clesSymbols = Object.getOwnPropertySymbols(entree);
for (const sym of clesSymbols) {
cible[sym] = cloneProfondComplet(entree[sym], cache);
}
return cible;
}
const cleSymbole = Symbol("cleUnique");
const valSymbole = Symbol("valeurUnique");
const ensemble = new Set([1, 2, 3]);
const dictionnaire = new Map([["nom", "Dupont"], ["age", 30]]);
const donneesSource = {
identifiant: "user_01",
profil: { nom: "Dupont", adresse: { ville: "Paris" } },
competences: ["JavaScript", "Node.js"],
executerTache() { return "Tâche en cours..."; },
[cleSymbole]: "valeur_cle",
metadata: valSymbole,
ensemble,
dictionnaire,
};
donneesSource.autoReference = donneesSource;
const copie = cloneProfondComplet(donneesSource);
console.log("Source originale:", donneesSource);
console.log("Copie modifiée:", copie);