Le passage de paramètres
Prédire ce qu’une fonction modifie réellement chez son appelant.
Objectifs
À la fin de cette leçon, vous saurez :
- énoncer la règle unique du passage de paramètres en JavaScript ;
- prédire, pour toute fonction, ce qu'elle modifie chez son appelant — et ce qu'elle n'y touche jamais ;
- reconnaître et maîtriser les effets de bord ;
- choisir entre style mutateur et style puriste en connaissance de cause.
🔗 Pour vous rafraîchir la mémoire : objets et références · découper avec des fonctions
La règle unique
Souvenez-vous de l'anatomie d'un appel (chapitre 3) : les arguments sont copiés dans les paramètres. JavaScript applique cela sans exception :
JavaScript passe toujours les paramètres par valeur. Mais pour un objet, la valeur transmise est… une copie de la référence.
Cette phrase explique tous les cas. Déroulons-la :
argument primitive → copie de la VALEUR → aucune répercussion possible
argument objet → copie de la RÉFÉRENCE → l'objet visé est PARTAGÉ
Le mot « copie » est présent dans les deux lignes : c'est lui qui interdit certaines choses ; c'est ce que la copie contient (valeur vs adresse) qui autorise les autres.
Cas 1 : primitive — rien ne sort
function changer(x) {
x = 99; // je remplace le contenu de MA copie locale
}
let nombre = 42;
changer(nombre);
console.log(nombre); // 42 : inchangé
Déroulé mémoire pendant l'appel :
avant l'appel : nombre → 42
pendant l'appel : x (copie locale) → 42, puis x → 99
après le retour : x est détruit ; nombre → 42, n'a jamais bougé
x et nombre sont deux cases distinctes qui ont eu le même contenu un instant. Aucun mécanisme ne permet à la fonction d'écrire dans la case de l'appelant.
Cas 2 : objet muté — tout se voit
function renommer(user) {
user.name = "Jean"; // MUTATION de l'objet pointé
}
const paul = { name: "Paul" };
renommer(paul);
console.log(paul.name); // "Jean" : changé !
Déroulé :
avant l'appel : paul ──→ { name: "Paul" }
pendant l'appel : user ──→ { name: "Paul" } ← même adresse, copiée
user.name = "Jean" mutation de L'OBJET
après le retour : paul ──→ { name: "Jean" } visible par l'appelant
La fonction n'a pas touché à la variable paul (impossible !) : elle a modifié l'objet que les deux variables partagent. Ce n'est pas une fuite du mécanisme, c'en est la conséquence directe.
Cas 3 : objet réaffecté — encore rien ne sort
Voici le cas qui piège tout le monde :
function remplacer(user) {
user = { name: "Jean" }; // RÉAFFECTATION de la référence LOCALE
}
const paul = { name: "Paul" };
remplacer(paul);
console.log(paul.name); // "Paul" : inchangé !
Déroulé :
avant l'appel : paul ──→ { name: "Paul" }
pendant l'appel : user ──→ { name: "Paul" }
user ──→ { name: "Jean" } ← NOUVEL objet,
seule la flèche LOCALE tourne
après le retour : paul ──→ { name: "Paul" } l'objet neuf est perdu
La réaffectation n'a jamais touché l'objet d'origine : elle a retourné la flèche user (locale) vers une maison neuve. À la fin de la fonction, cette maison neuve n'a plus de visiteur — le garbage collector la ramassera.
Récapitulons dans le tableau à connaître par cœur :
| Dans la fonction | Type de geste | Effet chez l'appelant |
|---|---|---|
param.propriete = … | mutation | visible |
array.push(...) etc. | mutation | visible |
param = nouvelleValeur | réaffectation | invisible |
| paramètre primitive, quel que soit le geste | — | invisible |
Test mental avant chaque écriture de fonction : « est-ce que je transforme l'objet pointé (visible), ou est-ce que je retourne ma flèche locale (invisible) ? »
Le tableau aussi, bien sûr
function ajouterTache(liste, tache) {
liste.push(tache); // mutation
}
const taches = ["dormir"];
ajouterTache(taches, "coder");
console.log(taches); // [ 'dormir', 'coder' ] : l'appelant voit grandir SON tableau
Utile quand c'est voulu (une file partagée), gênant quand ça ne l'était pas (une fonction « innocente » qui déforme les données de son appelant). Ce comportement porte un nom : effet de bord.
Maîtriser ses effets de bord : deux styles assumés
Deux façons honnêtes d'écrire une fonction qui « ajoute » :
// Style MUTATEUR : je modifie ce qu'on me donne, c'est mon contrat
function ajouter(liste, element) {
liste.push(element);
}
// Style PURISTE : je ne touche à rien, je rends du neuf
function ajouterSansEffet(liste, element) {
return [...liste, element]; // nouveau tableau (copie superficielle)
}
Comparaison honnête :
| Critère | Mutateur | Puriste |
|---|---|---|
| Mémoire / performance | économique (pas de copie) | coûte une copie par appel |
| Prévisibilité pour l'appelant | exige de lire la doc | totale : rien ne change dehors |
Composabilité (b(f(a))) | piégeuse | naturelle |
Règle pratique pour tout ce cursus : par défaut, puriste — une fonction reçoit des données, en retourne de nouvelles, ne touche à rien dehors. On passe au mutateur seulement quand la mutation est le but explicite (et alors on le nomme clairement : vider, trier, normaliserEnPlace...). Vous reverrez ce choix au chapitre 28 sous le nom de cohésion.
Un exemple complet du style puriste, avec vérification :
function normaliserTitres(liste) {
const resultat = [];
for (const titre of liste) {
resultat.push(titre.toUpperCase());
}
return resultat;
}
const titres = ["intro", "conclusion"];
const majuscules = normaliserTitres(titres);
console.log(titres); // [ 'intro', 'conclusion' ] : intact
console.log(majuscules); // [ 'INTRO', 'CONCLUSION' ]
Notez que resultat.push(...) est bien une mutation — mais d'un tableau local, créé par la fonction elle-même. Muter ses propres données est parfaitement sain ; muter celles des autres demande un contrat.
Exercice
Prédisez puis vérifiez chaque extrait.
function doubler(n) {
n = n * 2;
}
let valeur = 10;
doubler(valeur);
console.log(valeur);
function vider(liste) {
liste.length = 0; // oui, c'est permis : mutation
}
const panier = ["pain", "lait"];
vider(panier);
console.log(panier);
function echanger(a, b) {
const temp = a;
a = b;
b = temp;
}
let x = 1;
let y = 2;
echanger(x, y);
console.log(x, y);
-
Écrivez
sansDernier(liste)(style puriste) qui retourne un nouveau tableau privé du dernier élément, sans toucher à l'original. Vérifiez que l'original reste intact. -
Écrivez
echangerProprietes(objet)qui échange les valeurs des propriétésaetbd'un objet reçu. Pourquoi fonctionne-t-elle là où la question 3 échouait ? -
Expliquez en deux phrases, à quelqu'un qui n'a pas lu ce cours, pourquoi
remplacer(user)(cas 3) ne changeait rien chez l'appelant.
Résumé
- Passage toujours par valeur ; pour un objet, la valeur est une copie de référence.
- Mutation interne → visible dehors ; réaffectation interne → jamais.
- Les primitives sont totalement étanches ; seules les mutations d'objets traversent la frontière.
- Deux styles honnêtes : mutateur assumé ou puriste — par défaut, puriste.
- Avant d'écrire une fonction : décidez consciemment si elle mute les données de l'appelant.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Extrait 1. Affiche 10. n est une copie locale d'une primitive ; sa réaffectation meurt avec la fonction. Version utile de la fonction : return n * 2; puis valeur = doubler(valeur).
Extrait 2. Affiche []. Surprise voulue : liste.length = 0 est une mutation (on tronque le contenu du tableau pointé), donc visible par l'appelant. C'est un moyen réel (et violent) de vider un tableau partagé — à connaître pour le repérer aussi bien que pour l'utiliser.
Extrait 3. Affiche 1 2. L'échange classique échoue : a et b sont des copies locales de primitives ; les permuter ne permutent rien chez l'appelant. La fonction s'exécute correctement... sur des copies jetables. Voir question 5 pour la version qui marche.
Question 4.
function sansDernier(liste) {
return liste.slice(0, -1); // nouveau tableau, original intact
}
const source = ["a", "b", "c"];
const raccourci = sansDernier(source);
console.log(source); // [ 'a', 'b', 'c' ]
console.log(raccourci); // [ 'a', 'b' ]
console.log(source === raccourci); // false : deux objets distincts
Variante avec étalement : const copie = [...liste]; copie.pop(); return copie;. Le test final (=== false) est important : il prouve qu'on a bien créé un nouveau tableau plutôt que retouché l'original.
Question 5.
function echangerProprietes(objet) {
const temp = objet.a;
objet.a = objet.b;
objet.b = temp;
}
const paire = { a: 1, b: 2 };
echangerProprietes(paire);
console.log(paire); // { a: 2, b: 1 } : ça marche !
Pourquoi ici et pas en question 3 ? Parce que la fonction ne réaffecte aucun paramètre : elle mute les propriétés de l'objet pointé, objet partagé avec l'appelant. La permutation se fait à l'intérieur de la maison commune — visible par tous. C'est aussi le motif utilisé dans le monde réel pour « retourner plusieurs résultats » : passer un objet conteneur.
Question 6. Réponse attendue : « Le paramètre était une copie de l'adresse de l'objet, pas la variable de l'appelant. Réaffecter cette copie locale ne fait que pointer ailleurs localement ; l'objet d'origine et la variable restent intacts. »