Maîtriser l'instruction de retour des fonctions
Malgré sa simplicité, l'instruction return en JavaScript peut nous aider à améliorer la lisibilité de notre code et à le rendre moins complexe.
Cet article est libre d'accès pour tous grâce à ceux qui soutiennent notre blog indépendant.
Pour terminer une fonction, JavaScript utilise l'instruction return
. L'instruction return
est très simple. C'est d'ailleurs une des premières que l'on voit dans les cours de programmation.
Mais il ne faudrait pas sous-estimer sa capacité à nous aider à écrire un code plus propre et performant (et inversement lorsqu'on ne la maîtrise pas suffisamment).
J'utilise du code JavaScript pour illustrer mes propos dans cet article, mais les principes expliqués s'appliquent également aux autres langages de programmation.
Sommaire
Comment fonctionne l'instruction return
?
Nous utilisons les fonctions pour regrouper des instructions qui vont contextuellement ensemble. C'est-à-dire qu'elles servent à accomplir la même tâche.
Première règle que l'on s'efforcera de respecter : une fonction = une responsabilité. Jamais plus d'une responsabilité. Cela améliorera nettement notre code.
function addOneTo(a) {
const result = a + 1
return result
}
La fonction addOneTo(a)
ajoute 1 à la valeur contenue dans la variable a
. J'explique les variables en JavaScript un peu plus en détail dans un autre article, pour ceux qui ne sont pas encore très à l'aise avec le sujet.
L'instruction de retour nous permet de retourner le résultat de l'addition de cette façon : return result
.
En d'autres termes, l'instruction return
nous laisse indiquer clairement à l'interpréteur JavaScript ce que la fonction va retourner comme valeur. Dans notre exemple, il y a deux variables utilisées. Nous ne voulons pas retourner a
mais uniquement result
.
Qu'en est-il des fonctions sans instruction de retour ?
Ceux qui, comme moi, ont suivi des cours d'algorithmie se le rappelleront peut-être : une fonction doit toujours retourner quelque chose. C'est une règle immuable. JavaScript suit la même règle, les fonctions retournent donc toujours quelque chose. Dans les cas où nous ne souhaitons pas retourner de valeur, une autre structure existe. Ce sont les procédures.
Le problème, c'est qu'il n'existe pas de procédures en JavaScript.
Pour résoudre ce problème, JavaScript nous permet de retourner une valeur "indéfinie". Elle n'est pas définie. On ne sait pas ce qu'elle vaut. On peut dire qu'elle ne vaut rien.
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
return undefined
}
De cette façon, il est possible d'utiliser les fonctions comme des procédures, c'est-à-dire des rassemblements d'instructions réutilisables qui ne retournent pas de résultats définis.
La fonction prepareObject
exécute ses instructions et à la fin, nous indiquons que la fonction retourne la valeur undefined
. Ce qui pour nous veut dire que nous ne retournons rien, mais l'interpréteur est content parce que nous avons quand même retourné une valeur.
Si vous faites le test d'exécuter la fonction prepareObject
vous verrez que celle-ci retourne bien la valeur undefined
.
Rassurez-vous, il y a plusieurs manières de simplifier ce code. Tout d'abord, omettre la valeur comme ceci :
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
return
}
Comme nous ne retournons "rien", l'interpréteur de JavaScript comprend que vous souhaitez en fait retourner la valeur undefined
et fait automatiquement le remplacement.
Sinon vous pouvez aussi omettre toute l'instruction, de cette façon :
function prepareObject(obj) {
obj.connect({
// ...
})
obj.use({
// ...
})
}
Si vous vous dites que finalement il s'agit d'une procédure, c'est bien vu, mais c'est faux. Faites le test d'exécuter la fonction prepareObject
vous verrez que celle-ci continue de retourner la valeur undefined
.
L'instruction de retour est une instruction "bloquante"
Le fait que return
soit une instruction classique nous permet de la placer n'importe où dans le code d'une fonction.
function doComplicatedStuff(obj) {
obj.connect({
// ...
})
return obj
obj.use({
// ...
})
}
Le code précédent est tout à fait valide. La seule chose dont il faut se rappeler ici, c'est que return
est une instruction "bloquante" dans le scope de la fonction parent.
Ce qui veut dire qu'il indique l'arrêt de la fonction dans laquelle il se trouve. C'est important à comprendre, car cela signifie que le code situé après une instruction return
ne sera jamais exécuté.
Le code situé après une instruction return
est ce que l'on appelle du "code mort".
En fait, l'instruction return
veut littéralement dire : "cette fonction a fini de s'exécuter correctement, voici le résultat que tu peux remonter au contexte parent. Inutile de continuer à lire après.".
Mieux utiliser l'instruction return
Si vous vous êtes demandés pourquoi on utiliserait return;
au lieu de juste omettre totalement l'instruction, ou si vous vous êtes demandés pourquoi on écrirait du code après un return
, cette partie de l'article est pour vous.
Une fois que vous avez assimilé ces deux idées, il devient possible pour vous d'améliorer votre utilisation de l'instruction return
. Une bonne utilisation du return
permet d'améliorer la lisibilité du code.
Sortir le plus tôt possible de la fonction
Lorsque nous lisons du code, que ce soit notre code ou celui de quelqu'un d'autre, plus il y a de données à prendre en compte plus c'est compliqué à comprendre pour notre cerveau. Les sauts de contextes d'exécution font partie des données à considérer et chaque fonction est un nouveau contexte d'exécution.
Ceci dit, nous ne souhaitons pas utiliser moins les fonctions, car elles apportent beaucoup plus d'avantages que d'inconvénients (ré-utilisabilité, structuration du code, etc.)
Un moyen de réduire la complexité d'un code est de réduire le nombre de données "à prendre en compte". L'instruction return
peut nous aider à faire cela. Prenons un exemple.
function byOrder(a, b) {
let result = null
if (a.order > b.order) {
result = 1
}
if (a.order < b.order) {
result = -1
}
if (a.order == b.order) {
result = 0
}
return result
}
Cette fonction sert à indiquer à la fonction array.sort
de JavaScript comment trier un tableau. Elle s'utilise de la manière suivante array.sort(byOrder)
.
Lisons la fonction pour la comprendre. Détaillons le travail de notre cerveau pendant le débogage pour illustrer le point. a.order
veut 10
et b.order
vaut 0
.
- Tout d'abord, je vois qu'il y a une variable
result
initialisée avec la valeurnull
, mais sa valeur va changer, car on utilise l'instructionlet
. Pour en savoir plus sur cette étape, lire l'article détaillé sur les variables en JavaScript. - Une condition : si l'ordre de
a
est supérieur à celui deb
. C'est bien notre cas, on modifie la valeur deresult
par1
. - Une nouvelle condition : si l'ordre de
a
est inférieur à celui deb
. Ce n'est pas notre cas, pas la peine de rentrer dans le scope de cette condition. - Une nouvelle condition : si l'ordre de
a
est égale à celui deb
. Ce n'est pas notre cas, pas la peine de rentrer dans le scope de cette condition. - On rencontre l'instruction
return
qui veut retourner la valeur de la variableresult
. La fonction retourne1
car on est rentré dans le scope de la première condition.
Nous avons dû lire (et comprendre) beaucoup plus de code que nécessaire pour être sûr de comprendre ce que fait cette fonction. Les points 3 et 4 ci-dessus n'étaient pas indispensables mais nous avons quand même dû les exécuter dans notre cerveau. De plus, nous avons dû nous rappeler ce qui s'était passé avant ces étapes pour savoir quoi retourner.
Essayons maintenant de réécrire le code de byOrder
de façon à sortir le plus tôt possible grâce aux propriétés de return
. Ensuite, nous referons le même exercice pour comparer la complexité du code.
function byOrder(a, b) {
if (a.order > b.order) {
return 1
}
if (a.order < b.order) {
return -1
}
return 0
}
Cette nouvelle fonction fait exactement la même chose. Elle a la même signature que la première fonction. Essayons de la lire maintenant (pour rappel, a.order
veut 10
et b.order
vaut 0
) :
- Tout d'abord, j'entre dans la fonction et il y a directement une condition : si l'ordre de
a
est supérieur à celui deb
. C'est bien notre cas, on retourne la valeur 1.
Voilà. Comme l'instruction return
est bloquante et qu'elle arrête l'exécution de la fonction, dans notre cas spécifique, le reste du code est du code "mort". Cela ne sert à rien de le lire. Nous n'avons pas besoin de comprendre le reste de la fonction pour comprendre ce qu'elle fait dans notre cas.
Il y a donc beaucoup moins de paramètres à retenir pour le cerveau pendant la lecture. La complexité est réduite significativement. D'ailleurs, cela me fait penser au fonctionnement du mot-clé guard
en Swift.
Et, cela est vrai dans quasiment tous les cas. Essayez de refaire l'exercice pour a.order === b.order
, même s'il s'agit de la dernière des conditions testées, la deuxième fonction reste moins complexe.
Rendre l'utilisation du else
plus utile
Une autre façon d'améliorer le code de byOrder
est d'utiliser les mot-clés else
et else...if
.
function byOrder(a, b) {
let result = null
if (a.order > b.order) {
result = 1
} else if (a.order < b.order) {
result = -1
} else {
result = 0
}
return result
}
On reproduit ici le comportement de la solution avec les return
puisque les conditions suivantes ne sont pas exécutées dès qu'une condition correspondante est trouvée.
Par contre, la solution ne me convient pas, car je considère que cela reste chargé et cela me demande quand même plus de concentration que la solution des return
exposée plus haut.
Certains pallient ce problème en utilisant des return
, justement. Sauf qu'ils oublient l'intérêt des else
.
function byOrder(a, b) {
if (a.order > b.order) {
return 1
} else if (a.order < b.order) {
return -1
} else {
return 0
}
}
Dans ce cas précis, il faut choisir : soit les else
, soit les return
. L'utilisation des return
supprime tout l'intérêt des else
et, accessoirement, vient complexifier encore plus le code. Ce code n'a pas d'autre utilité que de montrer que celui qui l'a écrit n'avait pas bien compris les subtilités des instructions else
et des instructions return
. Ne faites pas ça.
En utilisant uniquement les return
dans cette fonction, vous supprimez quelques mots. Mais, surtout vous supprimez complètement un scope (le dernier else
) et vous supprimez un niveau d'indentation, ce qui, nous l'avons vu plus haut, contribue grandement à réduire la complexité de la fonction.
Personnellement, je n'utilise presque plus les else
dans mon code parce que j'arrive dans la plupart des cas à écrire une fonction beaucoup plus simple à lire et comprendre en utilisant exclusivement les return
.
Après, ne me faites pas dire ce que je n'ai pas dit, le else
et le else...if
restent des structures très utiles que je continue d'utiliser lorsque j'en ai besoin. Mon seul point ici est qu'ils ne sont vraiment utiles que dans certaines situations. Il ne sert à rien de les utiliser à tout-va s'il existe d'autres mots-clés ou d'autres structures de code mieux adaptés à la situation. Et ce conseil vaut pour à peu près tout ce qui existe en programmation.
En résumé, l'instruction de retour (return
) permet de retourner une valeur, définie ou non, au contexte parent de la fonction. Lorsque l'instruction de retour est rencontrée, elle met fin à l'exécution de la fonction courante.
Ceci aide à optimiser notre façon d'écrire du code afin de réduire le nombre de scopes et niveaux d'indentation différents au sein d'une même fonction, et par ailleurs de nous éviter d'avoir à tenir compte de trop d'informations lors du débogage.
Rejoins 250+ développeurs de notre liste de diffusion et sois reçois les articles directement dans ta boite mail.
Aucun spam. Désabonnes-toi en un seul clic à tout moment.
Si vous avez des questions ou des remarques/conseils, n'hésitez pas à laisser un commentaire plus bas ! Je serais ravis de vous lire. Et si vous aimez l'article, n'oubliez pas de le partager avec vos amis.