Aller au contenu principal

[Medium] 📄 Primitive vs Reference Types

1. What are Primitive Types and Reference Types?​

Que sont les types primitifs (Primitive Types) et les types de référence (Reference Types) ?

Les types de données en JavaScript se divisent en deux grandes catégories : les types primitifs et les types de référence. Ils présentent des différences fondamentales dans leur mode de stockage en mémoire et leur comportement lors du passage.

Types primitifs (Primitive Types)​

Caractéristiques :

  • Stockés dans le Stack (pile)
  • Lors du passage, la valeur elle-même est copiée (Call by Value)
  • Immuables (Immutable)

Comprennent 7 types :

// 1. String (chaîne de caractères)
const str = 'hello';

// 2. Number (nombre)
const num = 42;

// 3. Boolean (booléen)
const bool = true;

// 4. Undefined
let undef;

// 5. Null
const n = null;

// 6. Symbol (ES6)
const sym = Symbol('unique');

// 7. BigInt (ES2020)
const bigInt = 9007199254740991n;

Types de référence (Reference Types)​

Caractéristiques :

  • Stockés dans le Heap (tas)
  • Lors du passage, la référence (adresse mémoire) est copiée (Call by Reference)
  • Mutables (Mutable)

Comprennent :

// 1. Object (objet)
const obj = { name: 'John' };

// 2. Array (tableau)
const arr = [1, 2, 3];

// 3. Function (fonction)
const func = function () {};

// 4. Date
const date = new Date();

// 5. RegExp
const regex = /abc/;

// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();

2. Call by Value vs Call by Reference​

Passage par valeur (Call by Value) vs Passage par référence (Call by Reference)

Passage par valeur (Call by Value) - Types primitifs​

Comportement : La valeur elle-même est copiée ; modifier la copie n'affecte pas la valeur originale.

// Type primitif : passage par valeur
let a = 10;
let b = a; // Copier la valeur

b = 20; // Modifier b

console.log(a); // 10 (non affecté)
console.log(b); // 20

Diagramme mémoire :

┌─────────┐
│ Stack │
├─────────┤
│ a: 10 │ ← Valeur indépendante
├─────────┤
│ b: 20 │ ← Valeur indépendante (modifiée après copie)
└─────────┘

Passage par référence (Call by Reference) - Types de référence​

Comportement : L'adresse mémoire est copiée ; deux variables pointent vers le même objet.

// Type de référence : passage par référence
let obj1 = { name: 'John' };
let obj2 = obj1; // Copier l'adresse mémoire

obj2.name = 'Jane'; // Modifier via obj2

console.log(obj1.name); // 'Jane' (affecté !)
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true (pointent vers le même objet)

Diagramme mémoire :

┌─────────┐ ┌──────────────────┐
│ Stack │ │ Heap │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (même objet) │
└─────────┘ └──────────────────┘

3. Common Quiz Questions​

Questions de quiz courantes

Question 1 : Passage de types primitifs​

function changeValue(x) {
x = 100;
console.log('x dans la fonction :', x);
}

let num = 50;
changeValue(num);
console.log('num hors de la fonction :', num);
Cliquez pour voir la réponse
// x dans la fonction : 100
// num hors de la fonction : 50

Explication :

  • num est un type primitif (Number)
  • Lors du passage à la fonction, la valeur est copiée, x et num sont des variables indépendantes
  • Modifier x n'affecte pas num
// Flux d'exécution
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50 (copie)
x = 100; // Stack: x = 100 (seul x est modifié)
console.log(num); // Stack: num = 50 (non affecté)

Question 2 : Passage de types de référence​

function changeObject(obj) {
obj.name = 'Changed';
console.log('obj.name dans la fonction :', obj.name);
}

let person = { name: 'Original' };
changeObject(person);
console.log('person.name hors de la fonction :', person.name);
Cliquez pour voir la réponse
// obj.name dans la fonction : Changed
// person.name hors de la fonction : Changed

Explication :

  • person est un type de référence (Object)
  • Lors du passage à la fonction, l'adresse mémoire est copiée
  • obj et person pointent vers le même objet
  • Modifier le contenu de l'objet via obj affecte également person
// Diagramme mémoire
let person = { name: 'Original' }; // Heap: créer objet @0x001
changeObject(person); // Stack: obj = @0x001 (copier l'adresse)
obj.name = 'Changed'; // Heap: @0x001.name = 'Changed'
console.log(person.name); // Heap: @0x001.name (même objet)

Question 3 : Réassignation vs modification de propriété​

function test1(obj) {
obj.name = 'Modified'; // Modifier la propriété
}

function test2(obj) {
obj = { name: 'New Object' }; // Réassigner
}

let person = { name: 'Original' };

test1(person);
console.log('A:', person.name);

test2(person);
console.log('B:', person.name);
Cliquez pour voir la réponse
// A: Modified
// B: Modified (pas 'New Object' !)

Explication :

test1 : Modification de propriété

function test1(obj) {
obj.name = 'Modified'; // ✅ Modifie la propriété de l'objet original
}
// person et obj pointent vers le même objet, donc il est modifié

test2 : Réassignation

function test2(obj) {
obj = { name: 'New Object' }; // ❌ Change seulement la référence de obj
}
// obj pointe maintenant vers un nouvel objet, mais person pointe toujours vers l'original

Diagramme mémoire :

// Avant test1
person ────> { name: 'Original' }
obj ────> { name: 'Original' } (le même)

// Après test1
person ────> { name: 'Modified' }
obj ────> { name: 'Modified' } (le même)

// Exécution de test2
person ────> { name: 'Modified' } (inchangé)
obj ────> { name: 'New Object' } (nouvel objet)

// Après test2
person ────> { name: 'Modified' } (toujours inchangé)
// obj est détruit, le nouvel objet est récupéré par le garbage collector

Question 4 : Passage de tableaux​

function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}

function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}

let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);

reassignArray(numbers);
console.log('4:', numbers);
Cliquez pour voir la réponse
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]

Explication :

  • modifyArray : Modifie le contenu du tableau original, numbers est affecté
  • reassignArray : Change seulement la référence du paramètre, numbers n'est pas affecté

Question 5 : Opérations de comparaison​

// Comparaison de types primitifs
let a = 10;
let b = 10;
console.log('A:', a === b);

// Comparaison de types de référence
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
Cliquez pour voir la réponse
// A: true
// B: false
// C: true

Explication :

Types primitifs : Comparent les valeurs

10 === 10; // true (même valeur)

Types de référence : Comparent les adresses mémoire

obj1 === obj2; // false (objets différents, adresses différentes)
obj1 === obj3; // true (pointent vers le même objet)

Diagramme mémoire :

obj1 ────> @0x001: { value: 10 }
obj2 ────> @0x002: { value: 10 } (même contenu mais adresse différente)
obj3 ────> @0x001: { value: 10 } (même adresse que obj1)

4. Shallow Copy vs Deep Copy​

Copie superficielle vs Copie profonde

Copie superficielle (Shallow Copy)​

Définition : Copie uniquement le premier niveau ; les objets imbriqués restent des références.

Méthode 1 : Opérateur de décomposition (Spread Operator)​

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = { ...original };

// Modifier le premier niveau : n'affecte pas l'objet original
copy.name = 'Jane';
console.log(original.name); // 'John' (non affecté)

// Modifier l'objet imbriqué : affecte l'objet original !
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung' (affecté !)

Méthode 2 : Object.assign()​

const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);

copy.name = 'Jane';
console.log(original.name); // 'John' (non affecté)

Méthode 3 : Copie superficielle de tableaux​

const arr1 = [1, 2, 3];

// Méthode 1 : Opérateur de décomposition
const arr2 = [...arr1];

// Méthode 2 : slice()
const arr3 = arr1.slice();

// Méthode 3 : Array.from()
const arr4 = Array.from(arr1);

arr2[0] = 999;
console.log(arr1[0]); // 1 (non affecté)

Copie profonde (Deep Copy)​

Définition : Copie complètement tous les niveaux, y compris les objets imbriqués.

Méthode 1 : JSON.parse + JSON.stringify (la plus courante)​

const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};

const copy = JSON.parse(JSON.stringify(original));

// Modifier l'objet imbriqué : n'affecte pas l'objet original
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (non affecté)

copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming'] (non affecté)

Limitations :

const obj = {
date: new Date(), // ❌ Sera converti en chaîne
func: () => {}, // ❌ Sera ignoré
undef: undefined, // ❌ Sera ignoré
symbol: Symbol('test'), // ❌ Sera ignoré
regexp: /abc/, // ❌ Sera converti en {}
circular: null, // ❌ Les références circulaires causent une erreur
};
obj.circular = obj; // Référence circulaire

JSON.parse(JSON.stringify(obj)); // Erreur ou perte de données

Méthode 2 : structuredClone() (navigateurs modernes)​

const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};

const copy = structuredClone(original);

// Peut copier correctement les objets spéciaux comme Date
console.log(copy.date instanceof Date); // true

Avantages :

  • ✅ Supporte Date, RegExp, Map, Set, etc.
  • ✅ Supporte les références circulaires
  • ✅ Meilleures performances

Limitations :

  • ❌ Ne supporte pas les fonctions
  • ❌ Ne supporte pas Symbol

Méthode 3 : Implémentation récursive de la copie profonde​

function deepClone(obj) {
// Gérer null et les non-objets
if (obj === null || typeof obj !== 'object') {
return obj;
}

// Gérer les tableaux
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}

// Gérer Date
if (obj instanceof Date) {
return new Date(obj);
}

// Gérer RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}

// Gérer les objets
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}

return cloned;
}

// Exemple d'utilisation
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};

const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei' (non affecté)

Méthode 4 : Utiliser Lodash​

import _ from 'lodash';

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = _.cloneDeep(original);

Comparaison : Copie superficielle vs Copie profonde​

CaractéristiqueCopie superficielleCopie profonde
Niveaux copiésPremier niveau seulementTous les niveaux
Objets imbriquésRestent des référencesComplètement indépendants
PerformanceRapideLente
MémoirePeuBeaucoup
Cas d'utilisationObjets simplesStructures imbriquées complexes

5. Common Pitfalls​

Pièges courants

Piège 1 : Croire que le passage de paramètres peut modifier les types primitifs​

// ❌ Compréhension incorrecte
function increment(num) {
num = num + 1;
return num;
}

let count = 5;
increment(count);
console.log(count); // 5 (ne devient pas 6 !)

// ✅ Écriture correcte
count = increment(count); // Il faut recevoir la valeur de retour
console.log(count); // 6

Piège 2 : Croire que la réassignation peut modifier l'objet externe​

// ❌ Compréhension incorrecte
function resetObject(obj) {
obj = { name: 'Reset' }; // Change seulement la référence du paramètre
}

let person = { name: 'Original' };
resetObject(person);
console.log(person.name); // 'Original' (n'a pas été réinitialisé !)

// ✅ Écriture correcte 1 : Modifier les propriétés
function resetObject(obj) {
obj.name = 'Reset';
}

// ✅ Écriture correcte 2 : Retourner un nouvel objet
function resetObject(obj) {
return { name: 'Reset' };
}
person = resetObject(person);

Piège 3 : Croire que l'opérateur de décomposition fait une copie profonde​

// ❌ Compréhension incorrecte
const original = {
user: { name: 'John' },
};

const copy = { ...original }; // Copie superficielle !

copy.user.name = 'Jane';
console.log(original.user.name); // 'Jane' (a été affecté !)

// ✅ Écriture correcte : Copie profonde
const copy = JSON.parse(JSON.stringify(original));
// ou
const copy = structuredClone(original);

Piège 4 : Malentendu sur const​

// const empêche seulement la réassignation, ce n'est pas l'immutabilité !

const obj = { name: 'John' };

// ❌ Ne peut pas être réassigné
obj = { name: 'Jane' }; // TypeError: Assignment to constant variable

// ✅ Les propriétés peuvent être modifiées
obj.name = 'Jane'; // Fonctionne normalement
obj.age = 30; // Fonctionne normalement

// Pour une vraie immutabilité
const immutableObj = Object.freeze({ name: 'John' });
immutableObj.name = 'Jane'; // Échoue silencieusement (en mode strict, lance une erreur)
console.log(immutableObj.name); // 'John' (n'a pas été modifié)

Piège 5 : Problème de référence dans les boucles​

// ❌ Erreur courante
const arr = [];
const obj = { value: 0 };

for (let i = 0; i < 3; i++) {
obj.value = i;
arr.push(obj); // Tous pointent vers le même objet !
}

console.log(arr);
// [{ value: 2 }, { value: 2 }, { value: 2 }]
// Tous sont le même objet, la valeur finale est toujours 2

// ✅ Écriture correcte : Créer un nouvel objet à chaque fois
const arr = [];

for (let i = 0; i < 3; i++) {
arr.push({ value: i }); // Créer un nouvel objet à chaque fois
}

console.log(arr);
// [{ value: 0 }, { value: 1 }, { value: 2 }]

6. Best Practices​

Meilleures pratiques

✅ Approches recommandées​

// 1. Lors de la copie d'objets, utiliser explicitement des méthodes de copie
const original = { name: 'John', age: 30 };

// Copie superficielle (objets simples)
const copy1 = { ...original };

// Copie profonde (objets imbriqués)
const copy2 = structuredClone(original);

// 2. Les fonctions ne doivent pas dépendre des effets de bord pour modifier les paramètres
// ❌ Mauvais
function addItem(arr, item) {
arr.push(item); // Modifie le tableau original
}

// ✅ Bon
function addItem(arr, item) {
return [...arr, item]; // Retourne un nouveau tableau
}

// 3. Utiliser const pour prévenir les réassignations accidentelles
const config = { theme: 'dark' };
// config = {}; // Lancera une erreur

// 4. Utiliser Object.freeze quand des objets immuables sont nécessaires
const constants = Object.freeze({
PI: 3.14159,
MAX_SIZE: 100,
});

❌ Approches à éviter​

// 1. Ne pas dépendre du passage de paramètres pour modifier les types primitifs
function increment(num) {
num++; // ❌ Sans effet
}

// 2. Ne pas confondre copie superficielle et copie profonde
const copy = { ...nested }; // ❌ Croire que c'est une copie profonde

// 3. Ne pas réutiliser la même référence d'objet dans les boucles
for (let i = 0; i < 3; i++) {
arr.push(obj); // ❌ Tous pointent vers le même objet
}

7. Interview Summary​

Résumé pour l'entretien

Mémorisation rapide​

Types primitifs (Primitive) :

  • String, Number, Boolean, Undefined, Null, Symbol, BigInt
  • Passage par valeur (Call by Value)
  • Stockés dans le Stack
  • Immuables (Immutable)

Types de référence (Reference) :

  • Object, Array, Function, Date, RegExp, etc.
  • Passage par référence (Call by Reference)
  • Stockés dans le Heap
  • Mutables (Mutable)

Exemple de réponse en entretien​

Q : JavaScript est-il Call by Value ou Call by Reference ?

JavaScript est Call by Value pour tous les types, mais la « valeur » passée pour les types de référence est l'adresse mémoire.

  • Types primitifs : Une copie de la valeur est passée, les modifications n'affectent pas la valeur originale
  • Types de référence : Une copie de l'adresse est passée, via l'adresse on peut modifier l'objet original
  • Cependant, si on réassigne (change l'adresse), l'objet original n'est pas affecté

Reference​