Code JavaScript bogué : les 10 erreurs les plus courantes commises par les développeurs JavaScript

Publié: 2022-03-11

Aujourd'hui, JavaScript est au cœur de pratiquement toutes les applications Web modernes. Ces dernières années en particulier ont vu la prolifération d'un large éventail de puissantes bibliothèques et frameworks basés sur JavaScript pour le développement d'applications monopage (SPA), les graphiques et l'animation, et même les plates-formes JavaScript côté serveur. JavaScript est véritablement devenu omniprésent dans le monde du développement d'applications Web et est donc une compétence de plus en plus importante à maîtriser.

À première vue, JavaScript peut sembler assez simple. Et en effet, intégrer des fonctionnalités JavaScript de base dans une page Web est une tâche assez simple pour tout développeur de logiciel expérimenté, même s'il est novice en JavaScript. Pourtant, le langage est nettement plus nuancé, puissant et complexe qu'on ne pourrait le croire au départ. En effet, de nombreuses subtilités de JavaScript entraînent un certain nombre de problèmes courants qui l'empêchent de fonctionner - dont 10 sont abordés ici - qu'il est important de connaître et d'éviter dans sa quête pour devenir un maître développeur JavaScript.

Erreur courante n° 1 : références incorrectes à this

J'ai entendu une fois un comédien dire :

Je ne suis pas vraiment là, car qu'est-ce qu'il y a ici, à part là-bas, sans le 't' ?

Cette blague caractérise à bien des égards le type de confusion qui existe souvent pour les développeurs concernant le mot-clé this de JavaScript. Je veux dire, est this vraiment cela, ou est-ce tout autre chose ? Ou est-ce indéfini?

Comme les techniques de codage JavaScript et les modèles de conception sont devenus de plus en plus sophistiqués au fil des ans, il y a eu une augmentation correspondante de la prolifération des étendues d'auto-référence dans les rappels et les fermetures, qui sont une source assez courante de « ceci/cela confusion ».

Considérez cet exemple d'extrait de code :

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

L'exécution du code ci-dessus génère l'erreur suivante :

 Uncaught TypeError: undefined is not a function

Pourquoi?

Tout est question de contexte. La raison pour laquelle vous obtenez l'erreur ci-dessus est que, lorsque vous appelez setTimeout() , vous appelez en fait window.setTimeout() . Par conséquent, la fonction anonyme transmise à setTimeout() est définie dans le contexte de l'objet window , qui n'a pas de méthode clearBoard() .

Une solution traditionnelle, compatible avec les anciens navigateurs, consiste simplement à enregistrer votre référence à this dans une variable qui peut ensuite être héritée par la fermeture ; par exemple:

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

Alternativement, dans les navigateurs plus récents, vous pouvez utiliser la méthode bind() pour transmettre la référence appropriée :

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };

Erreur courante n° 2 : penser qu'il existe une portée au niveau du bloc

Comme indiqué dans notre guide de recrutement JavaScript, une source courante de confusion parmi les développeurs JavaScript (et donc une source courante de bogues) suppose que JavaScript crée une nouvelle portée pour chaque bloc de code. Bien que cela soit vrai dans de nombreux autres langages, ce n'est pas vrai en JavaScript. Considérez, par exemple, le code suivant :

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

Si vous devinez que l'appel console.log() produirait un résultat undefined ou générerait une erreur, vous avez mal deviné. Croyez-le ou non, il affichera 10 . Pourquoi?

Dans la plupart des autres langages, le code ci-dessus conduirait à une erreur car la "vie" (c'est-à-dire la portée) de la variable i serait limitée au bloc for . En JavaScript, cependant, ce n'est pas le cas et la variable i reste dans la portée même après la fin de la boucle for , conservant sa dernière valeur après la sortie de la boucle. (Ce comportement est connu d'ailleurs sous le nom de levage variable).

Il convient de noter, cependant, que la prise en charge des portées au niveau des blocs fait son chemin dans JavaScript via le nouveau mot-clé let . Le mot clé let est déjà disponible dans JavaScript 1.7 et devrait devenir un mot clé JavaScript officiellement pris en charge à partir d'ECMAScript 6.

Nouveau sur JavaScript ? Renseignez-vous sur les portées, les prototypes et plus encore.

Erreur courante n° 3 : créer des fuites de mémoire

Les fuites de mémoire sont des problèmes JavaScript presque inévitables si vous ne codez pas consciemment pour les éviter. Il existe de nombreuses façons pour eux de se produire, nous allons donc souligner quelques-unes de leurs occurrences les plus courantes.

Exemple de fuite de mémoire 1 : références pendantes à des objets obsolètes

Considérez le code suivant :

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

Si vous exécutez le code ci-dessus et surveillez l'utilisation de la mémoire, vous constaterez que vous avez une fuite de mémoire massive, perdant un mégaoctet complet par seconde ! Et même un GC manuel n'aide pas. Il semble donc que nous longStr chaque fois replaceThing est appelé. Mais pourquoi?

Examinons les choses plus en détail :

Chaque objet theThing contient son propre objet longStr 1 Mo. Chaque seconde, lorsque nous appelons replaceThing , il conserve une référence à l'objet theThing précédent dans priorThing . Mais nous ne pensons toujours pas que ce serait un problème, car à chaque fois, le priorThing précédemment référencé serait déréférencé (lorsque priorThing est réinitialisé via priorThing = theThing; ). Et de plus, n'est référencé que dans le corps principal de replaceThing et dans la fonction unused qui n'est, en fait, jamais utilisée.

Encore une fois, nous nous demandons pourquoi il y a une fuite de mémoire ici !?

Pour comprendre ce qui se passe, nous devons mieux comprendre comment les choses fonctionnent en JavaScript sous le capot. La manière typique dont les fermetures sont implémentées est que chaque objet fonction a un lien vers un objet de style dictionnaire représentant sa portée lexicale. Si les deux fonctions définies à l'intérieur replaceThing utilisaient réellement priorThing , il serait important qu'elles obtiennent toutes les deux le même objet, même si priorThing est assigné à maintes reprises, de sorte que les deux fonctions partagent le même environnement lexical. Mais dès qu'une variable est utilisée par une fermeture, elle se retrouve dans l'environnement lexical partagé par toutes les fermetures de cette portée. Et cette petite nuance est ce qui conduit à cette fuite de mémoire noueuse. (Plus de détails à ce sujet sont disponibles ici.)

Exemple de fuite de mémoire 2 : références circulaires

Considérez ce fragment de code :

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

Ici, onClick a une fermeture qui garde une référence à element (via element.nodeName ). En attribuant également onClick à element.click , la référence circulaire est créée ; c'est-à-dire: element -> onClick -> element -> onClick -> element

Fait intéressant, même si element est supprimé du DOM, l'auto-référence circulaire ci-dessus empêcherait la collecte de l' element et de onClick , et donc une fuite de mémoire.

Éviter les fuites de mémoire : ce que vous devez savoir

La gestion de la mémoire de JavaScript (et, en particulier, le ramasse-miettes) est largement basée sur la notion d'accessibilité des objets.

Les objets suivants sont supposés être accessibles et sont appelés "racines":

  • Objets référencés depuis n'importe où dans la pile d'appels actuelle (c'est-à-dire, toutes les variables et tous les paramètres locaux dans les fonctions actuellement invoquées, et toutes les variables dans la portée de fermeture)
  • Toutes les variables globales

Les objets sont conservés en mémoire au moins aussi longtemps qu'ils sont accessibles à partir de l'une des racines via une référence ou une chaîne de références.

Il y a un Garbage Collector (GC) dans le navigateur qui nettoie la mémoire occupée par des objets inaccessibles ; c'est-à-dire que les objets seront supprimés de la mémoire si et seulement si le GC estime qu'ils sont inaccessibles. Malheureusement, il est assez facile de se retrouver avec des objets "zombies" disparus qui ne sont en fait plus utilisés mais que le GC pense toujours être "accessibles".

En relation: Meilleures pratiques et astuces JavaScript par les développeurs Toptal

Erreur courante n°4 : Confusion sur l'égalité

L'un des avantages de JavaScript est qu'il contraint automatiquement toute valeur référencée dans un contexte booléen à une valeur booléenne. Mais il y a des cas où cela peut être aussi déroutant que pratique. Certains des éléments suivants, par exemple, sont connus pour mordre de nombreux développeurs JavaScript :

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

En ce qui concerne les deux derniers, bien qu'ils soient vides (ce qui pourrait laisser croire qu'ils seraient évalués à false ), {} et [] sont en fait des objets et tout objet sera contraint à une valeur booléenne true en JavaScript, conforme à la spécification ECMA-262.

Comme ces exemples le démontrent, les règles de coercition de type peuvent parfois être claires comme de la boue. Par conséquent, à moins que la coercition de type ne soit explicitement souhaitée, il est généralement préférable d'utiliser === et !== (plutôt que == et != ), afin d'éviter tout effet secondaire involontaire de la coercition de type. ( == et != effectuent automatiquement la conversion de type lors de la comparaison de deux éléments, tandis que === et !== effectuent la même comparaison sans conversion de type.)

Et complètement comme un point secondaire - mais puisque nous parlons de coercition de type et de comparaisons - il convient de mentionner que comparer NaN avec n'importe quoi (même NaN !) renverra toujours false . Vous ne pouvez donc pas utiliser les opérateurs d'égalité ( == , === , != , !== ) pour déterminer si une valeur est NaN ou non. À la place, utilisez la fonction intégrée globale isNaN() :

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

Erreur courante #5 : Manipulation inefficace du DOM

JavaScript rend relativement facile la manipulation du DOM (c'est-à-dire l'ajout, la modification et la suppression d'éléments), mais ne fait rien pour favoriser une telle efficacité.

Un exemple courant est le code qui ajoute une série d'éléments DOM un par un. L'ajout d'un élément DOM est une opération coûteuse. Le code qui ajoute plusieurs éléments DOM consécutivement est inefficace et risque de ne pas fonctionner correctement.

Une alternative efficace lorsque plusieurs éléments DOM doivent être ajoutés consiste à utiliser des fragments de document à la place, améliorant ainsi à la fois l'efficacité et les performances.

Par exemple:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

En plus de l'efficacité intrinsèquement améliorée de cette approche, la création d'éléments DOM attachés est coûteuse, alors que les créer et les modifier tout en étant détachés, puis les attacher donne de bien meilleures performances.

Erreur courante n° 6 : Utilisation incorrecte des définitions de fonctions dans les boucles for

Considérez ce code :

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }

Sur la base du code ci-dessus, s'il y avait 10 éléments d'entrée, cliquer sur l'un d'eux afficherait "Ceci est l'élément #10" ! En effet, au moment où onclick est appelé pour l'un des éléments, la boucle for ci-dessus sera terminée et la valeur de i sera déjà 10 (pour tous ).

Voici comment nous pouvons corriger les problèmes de code ci-dessus, cependant, pour obtenir le comportement souhaité :

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }

Dans cette version révisée du code, makeHandler est immédiatement exécuté chaque fois que nous traversons la boucle, recevant à chaque fois la valeur actuelle de i+1 et la liant à une variable num délimitée. La fonction externe renvoie la fonction interne (qui utilise également cette variable num délimitée) et onclick de l'élément est défini sur cette fonction interne. Cela garantit que chaque onclick reçoit et utilise la valeur i appropriée (via la variable num délimitée).

Erreur courante n° 7 : ne pas exploiter correctement l'héritage prototype

Un pourcentage étonnamment élevé de développeurs JavaScript ne parviennent pas à comprendre pleinement, et donc à exploiter pleinement, les fonctionnalités de l'héritage prototypique.

Voici un exemple simple. Considérez ce code :

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

Cela semble assez simple. Si vous fournissez un nom, utilisez-le, sinon définissez le nom sur 'default' ; par exemple:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

Mais que se passerait-il si nous devions faire ceci :

 delete secondObj.name;

On obtiendrait alors :

 console.log(secondObj.name); // -> Results in 'undefined'

Mais ne serait-il pas plus agréable que cela revienne à "par défaut" ? Cela peut facilement être fait, si nous modifions le code d'origine pour tirer parti de l'héritage prototypique, comme suit :

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

Avec cette version, BaseObject hérite de la propriété name de son objet prototype , où elle est définie (par défaut) sur 'default' . Ainsi, si le constructeur est appelé sans nom, le nom par défaut sera default . Et de même, si la propriété name est supprimée d'une instance de BaseObject , la chaîne prototype sera alors recherchée et la propriété name sera récupérée à partir de l'objet prototype où sa valeur est toujours 'default' . Alors maintenant on obtient :

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

Erreur courante n° 8 : créer des références incorrectes aux méthodes d'instance

Définissons un objet simple et créons-en une instance, comme suit :

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

Maintenant, pour plus de commodité, créons une référence à la méthode whoAmI , probablement pour pouvoir y accéder simplement par whoAmI() plutôt que par le plus long obj.whoAmI() :

 var whoAmI = obj.whoAmI;

Et juste pour être sûr que tout semble copacétique, imprimons la valeur de notre nouvelle variable whoAmI :

 console.log(whoAmI);

Les sorties:

 function () { console.log(this === window ? "window" : "MyObj"); }

OK cool. Semble bien.

Mais maintenant, regardez la différence lorsque nous obj.whoAmI() par rapport à notre référence de commodité whoAmI() :

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

Qu'est ce qui ne s'est pas bien passé?

Le headfake ici est que, lorsque nous avons fait l'affectation var whoAmI = obj.whoAmI; , la nouvelle variable whoAmI était définie dans l'espace de noms global . Par conséquent, sa valeur de this est window , pas l'instance obj de MyObject !

Ainsi, si nous avons vraiment besoin de créer une référence à une méthode existante d'un objet, nous devons être sûrs de le faire dans l'espace de noms de cet objet, pour préserver la valeur de this . Une façon de procéder serait, par exemple, comme suit :

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

Erreur courante n° 9 : fournir une chaîne comme premier argument à setTimeout ou setInterval

Pour commencer, soyons clairs sur quelque chose ici : fournir une chaîne comme premier argument à setTimeout ou setInterval n'est pas en soi une erreur. C'est du code JavaScript parfaitement légitime. La question ici est plus une question de performance et d'efficacité. Ce qui est rarement expliqué, c'est que, sous le capot, si vous transmettez une chaîne comme premier argument à setTimeout ou setInterval , elle sera transmise au constructeur de la fonction pour être convertie en une nouvelle fonction. Ce processus peut être lent et inefficace, et est rarement nécessaire.

L'alternative au passage d'une chaîne comme premier argument à ces méthodes est de passer à la place une fonction . Prenons un exemple.

Voici donc une utilisation assez typique de setInterval et setTimeout , en passant une chaîne comme premier paramètre :

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

Le meilleur choix serait de passer une fonction comme argument initial ; par exemple:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

Erreur courante n° 10 : Ne pas utiliser le « mode strict »

Comme expliqué dans notre guide d'embauche JavaScript, le "mode strict" (c'est-à-dire, y compris 'use strict'; au début de vos fichiers source JavaScript) est un moyen d'appliquer volontairement une analyse et une gestion des erreurs plus strictes sur votre code JavaScript au moment de l'exécution, ainsi comme le rendant plus sûr.

S'il est vrai que ne pas utiliser le mode strict n'est pas une « erreur » en soi, son utilisation est de plus en plus encouragée et son omission est de plus en plus considérée comme une mauvaise forme.

Voici quelques avantages clés du mode strict :

  • Facilite le débogage. Les erreurs de code qui auraient autrement été ignorées ou auraient échoué silencieusement généreront désormais des erreurs ou lèveront des exceptions, vous alertant plus tôt des problèmes dans votre code et vous dirigeant plus rapidement vers leur source.
  • Empêche les globales accidentelles. Sans mode strict, l'affectation d'une valeur à une variable non déclarée crée automatiquement une variable globale portant ce nom. C'est l'une des erreurs les plus courantes en JavaScript. En mode strict, tenter de le faire génère une erreur.
  • Élimine this coercition . Sans mode strict, une référence à une valeur this de null ou undefined est automatiquement convertie en global. Cela peut provoquer de nombreux headfakes et des bugs qui vous arrachent les cheveux. En mode strict, faire référence à this valeur de null ou undefined génère une erreur.
  • Interdit les noms de propriété ou les valeurs de paramètre en double. Le mode strict renvoie une erreur lorsqu'il détecte une propriété nommée en double dans un objet (par exemple, var object = {foo: "bar", foo: "baz"}; ) ou un argument nommé en double pour une fonction (par exemple, function foo(val1, val2, val1){} ), attrapant ainsi ce qui est presque certainement un bogue dans votre code que vous auriez autrement perdu beaucoup de temps à traquer.
  • Rend eval() plus sûr. Il existe quelques différences dans la façon dont eval() se comporte en mode strict et en mode non strict. Plus important encore, en mode strict, les variables et les fonctions déclarées à l'intérieur d'une instruction eval() ne sont pas créées dans la portée contenante (elles sont créées dans la portée contenante en mode non strict, ce qui peut également être une source courante de problèmes).
  • Génère une erreur en cas d'utilisation invalide de delete . L'opérateur de delete (utilisé pour supprimer les propriétés des objets) ne peut pas être utilisé sur les propriétés non configurables de l'objet. Le code non strict échouera silencieusement lors d'une tentative de suppression d'une propriété non configurable, tandis que le mode strict générera une erreur dans un tel cas.

Emballer

Comme c'est le cas avec toute technologie, mieux vous comprendrez pourquoi et comment JavaScript fonctionne et ne fonctionne pas, plus votre code sera solide et plus vous pourrez exploiter efficacement la véritable puissance du langage. Inversement, le manque de bonne compréhension des paradigmes et concepts JavaScript est en effet à l'origine de nombreux problèmes JavaScript.

Bien se familiariser avec les nuances et les subtilités de la langue est la stratégie la plus efficace pour améliorer votre maîtrise et augmenter votre productivité. Éviter de nombreuses erreurs JavaScript courantes vous aidera lorsque votre JavaScript ne fonctionne pas.

Connexes : Promesses JavaScript : un didacticiel avec des exemples