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