frontend / javascript
structuredClone et sérialisation avancée
Explication
Ce que vous allez apprendre
- Identifier les limites de
JSON.parse(JSON.stringify(x))pour copier une structure riche - Utiliser
structuredClone()pour copier fiablement dates, Map, Set et structures circulaires - Expliquer pourquoi une instance de classe clonée perd son prototype et ses méthodes
- Personnaliser une sérialisation avec un replacer/reviver ou une méthode
toJSON() - Choisir la bonne technique de copie selon le type de données à dupliquer
Dans quel contexte ?
Un développeur sauvegarde l'état d'un formulaire dans le stockage local avant de le restaurer plus tard avec JSON.parse(JSON.stringify(etatFormulaire)). Après restauration, un champ dateEcheance qui était un objet Date devient une simple chaîne de caractères, cassant tous les calculs de délai qui suivent (dateEcheance.getTime is not a function). Remplacer cette technique par structuredClone(etatFormulaire) préserve correctement le type Date, sans conversion silencieuse en chaîne.
1. Copier semble trivial, jusqu'à ce que ça se complique
Copier une valeur semble simple, jusqu'à ce qu'on essaie de copier une structure un peu riche : une date, une Map, ou un objet contenant une référence vers lui-même.
Prérequis
Cette leçon prolonge directement la leçon 4 sur le spread et la copie superficielle : gardez en tête que { ...objet } ne copie que le premier niveau, ce qui motive en partie le besoin d'une solution plus robuste comme structuredClone.
2. La technique la plus répandue a ses limites
JSON.parse(JSON.stringify(x)) perd silencieusement une grande partie de ces informations : une Date redevient une simple chaîne, une Map disparaît totalement, et une référence circulaire fait carrément planter le code.
| Technique | Dates | Map/Set | Références circulaires | Fonctions/classes |
|---|---|---|---|---|
JSON.parse(JSON.stringify(x)) | Devient une chaîne | Perdues | Erreur | Perdues |
structuredClone(x) | Préservées | Préservées | Gérées correctement | Perdues (prototype non conservé) |
3. Une meilleure solution : structuredClone
structuredClone(), une API native, résout la plupart de ces limitations : elle préserve les types natifs (Date, Map, Set) et gère correctement les structures circulaires.
4. D'où vient cette fiabilité
Elle s'appuie sur le même algorithme utilisé en interne par postMessage, déjà croisé dans les leçons sur les Web Workers et les Service Workers.
5. Ses propres limites à connaître
structuredClone ne peut cloner ni fonctions ni instances de classes personnalisées : une instance clonée perd son prototype et redevient un simple objet littéral, avec les données mais sans les méthodes.
Piège fréquent
structuredClone(instanceDeMaClasse) renvoie un objet qui a les mêmes données, mais instanceof MaClasse renverra false sur le résultat : toutes les méthodes de la classe sont perdues. Pour cloner une instance en conservant son comportement, il faut réinstancier explicitement la classe avec les données clonées.
6. Pour les cas vraiment spécifiques
Quand ni JSON ni structuredClone ne suffisent, le "replacer"/"reviver" de JSON.stringify/parse, ou une méthode toJSON() sur une classe, permettent de personnaliser précisément la sérialisation.
Cette leçon relie plusieurs fils du cours : le spread superficiel (leçon 4), les classes (leçon 6) et la communication inter-thread (leçons 25-26) trouvent ici leur dénominateur commun.
Commandes & code
structuredClone et sérialisation avancée
Copier et transmettre des structures de données complexes correctement.
// --- Les limites de JSON.stringify/parse pour cloner ---
const original = {
date: new Date(),
ensemble: new Set([1, 2, 3]),
carte: new Map([["a", 1]]),
fonction: () => 42,
indefini: undefined,
grandEntier: 123n,
tableau: new Uint8Array([1, 2, 3]),
};
const clonJSON = JSON.parse(JSON.stringify(original));
console.log(clonJSON.date); // string, PAS un objet Date
console.log(clonJSON.ensemble); // undefined -- Set totalement perdu
console.log(clonJSON.carte); // undefined -- Map totalement perdue
console.log(clonJSON.fonction); // undefined -- les fonctions sont ignorees
console.log("grandEntier" in clonJSON); // JSON.stringify LEVE une TypeError sur un BigInt
// --- structuredClone() : l'algorithme natif du navigateur, bien plus complet ---
const clone = structuredClone(original);
console.log(clone.date instanceof Date); // true -- type preserve
console.log(clone.ensemble instanceof Set); // true
console.log(clone.carte instanceof Map); // true
console.log(clone.tableau instanceof Uint8Array); // true
// console.log(structuredClone({ f: () => {} })); // DataCloneError : les fonctions ne sont PAS clonables
// --- structuredClone gere les references CIRCULAIRES, contrairement a JSON ---
const objetCirculaire = { nom: "noeud" };
objetCirculaire.self = objetCirculaire;
// JSON.stringify(objetCirculaire); // TypeError : Converting circular structure to JSON
const cloneCirculaire = structuredClone(objetCirculaire);
console.log(cloneCirculaire.self === cloneCirculaire); // true -- la circularite est preservee
// --- Ce que structuredClone ne peut PAS cloner ---
// Fonctions, Symbols, prototypes personnalises (perd la classe -- devient un objet simple),
// descripteurs de proprietes (getters/setters), et certains objets DOM specifiques.
class Point {
constructor(x, y) { this.x = x; this.y = y; }
distance() { return Math.sqrt(this.x ** 2 + this.y ** 2); }
}
const pointOriginal = new Point(3, 4);
const pointClone = structuredClone(pointOriginal);
console.log(pointClone instanceof Point); // false -- redevient un objet litteral simple
console.log(pointClone.x, pointClone.y); // donnees preservees, methodes perdues
// --- JSON avec replacer/reviver : serialisation CUSTOM pour des types non supportes ---
function remplacer(cle, valeur) {
if (valeur instanceof Map) {
return { __type: "Map", donnees: Array.from(valeur.entries()) };
}
if (typeof valeur === "bigint") {
return { __type: "BigInt", donnees: valeur.toString() };
}
return valeur;
}
function reviveur(cle, valeur) {
if (valeur && valeur.__type === "Map") return new Map(valeur.donnees);
if (valeur && valeur.__type === "BigInt") return BigInt(valeur.donnees);
return valeur;
}
const donnees = { carte: new Map([["x", 1]]), id: 9007199254740993n };
const json = JSON.stringify(donnees, remplacer);
const relu = JSON.parse(json, reviveur);
console.log(relu.carte instanceof Map, relu.id);
// --- toJSON() : personnaliser automatiquement la serialisation d'une classe ---
class Argent {
constructor(montant, devise) {
this.montant = montant;
this.devise = devise;
}
toJSON() {
// appele AUTOMATIQUEMENT par JSON.stringify, sans passer de replacer
return `${this.montant} ${this.devise}`;
}
}
console.log(JSON.stringify({ prix: new Argent(49.99, "EUR") }));
// --- postMessage entre iframes/workers : utilise structuredClone en interne ---
// worker.postMessage({ carte: new Map([["cle", "valeur"]]) }); // fonctionne nativement,
// contrairement a JSON.stringify qui perdrait la Map avant l'envoi
// --- Transferable objects : deplacer plutot que cloner (voir lecon Web Workers) ---
function envoyerSansCopie(worker, buffer) {
worker.postMessage({ buffer }, [buffer]); // transfert de propriete, zero copie
}
// --- Comparaison structurelle profonde (utile pour des tests, du memoization) ---
function egaliteProfonde(a, b) {
if (a === b) return true;
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
const clesA = Object.keys(a);
const clesB = Object.keys(b);
if (clesA.length !== clesB.length) return false;
return clesA.every((cle) => egaliteProfonde(a[cle], b[cle]));
}
console.log(egaliteProfonde({ a: { b: 1 } }, { a: { b: 1 } })); // true| Méthode | Fonctions | Dates/Map/Set | Cycles | Classes |
|---|---|---|---|---|
JSON.stringify/parse | ignorées | perdues | erreur | perdues |
structuredClone | erreur (non clonable) | préservées | supportés | redevient objet simple |
Résumé
JSON.stringify/parseperd lesDate,Map,Set,undefined,BigIntet échoue sur les références circulaires.structuredClone()préserve ces types natifs et gère les cycles, mais ne clone ni fonctions ni instances de classe (perd le prototype).- Un
replacer/revivercustom (outoJSON()sur une classe) comble les lacunes deJSON.stringifypour des types spécifiques. postMessage(Workers, iframes) utilisestructuredCloneen interne, contrairement à une sérialisation JSON manuelle.
Exercices pratiques
Mission : un panier qui perd ses méthodes
Objectif : Diagnostiquer pourquoi une instance de classe clonée perd son comportement, et choisir la bonne technique de copie selon le besoin.
Contexte
Un développeur clone une instance de Panier (une classe avec une méthode total()) avant de l'envoyer à un Worker :
class Panier {
constructor(articles) { this.articles = articles; }
total() { return this.articles.reduce((s, a) => s + a.prix, 0); }
}
const monPanier = new Panier([{ prix: 10 }, { prix: 25 }]);
const panierClone = structuredClone(monPanier);
panierClone.total(); // TypeError : panierClone.total is not a functionpanierClone.articles contient pourtant bien les deux articles attendus.