Retour au cours

frontend / javascript

structuredClone et sérialisation avancée

Leçon 291 exercice

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.

TechniqueDatesMap/SetRéférences circulairesFonctions/classes
JSON.parse(JSON.stringify(x))Devient une chaînePerduesErreurPerdues
structuredClone(x)PréservéesPréservéesGérées correctementPerdues (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.

js
// --- 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éthodeFonctionsDates/Map/SetCyclesClasses
JSON.stringify/parseignoréesperdueserreurperdues
structuredCloneerreur (non clonable)préservéessupportésredevient objet simple

Résumé

  • JSON.stringify/parse perd les Date, Map, Set, undefined, BigInt et é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/reviver custom (ou toJSON() sur une classe) comble les lacunes de JSON.stringify pour des types spécifiques.
  • postMessage (Workers, iframes) utilise structuredClone en interne, contrairement à une sérialisation JSON manuelle.

Exercices pratiques

1 disponible
1

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 :

js
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 function

panierClone.articles contient pourtant bien les deux articles attendus.

Résoudre l’exercice →