Javascript : ce que j’avais pas compris

Javascript : ce que j’avais pas compris

Javascript est un l’un des langages les plus accessibles. Mais entre ceux qui l’utilisent et ceux qui le maîtrisent, il y a une nette différence. Javascript est rempli de nuances, de comportements flous et de concepts cachés. Ça va te rendre barjo si tu ne les connais pas.



Le piège Javascript

Il y a bien longtemps, dans une galaxie lointaine, très lointaine je faisais mon arrivée dans une nouvelle équipe. Derrière moi, une forte spécialisation PHP. Ce jour était important. Je laissais tomber ma religion PHP pour intégrer une équipe de religion Javascript.

À ce moment-là, je suis persuadé de deux choses. Javascript c’est facile et je maîtrise déjà parfaitement. Pas besoin de vraiment comprendre comment fonctionnent les entrailles du langage pour l’utiliser. Ça va bien aller.

Mais rapidement, j’ai commencé à voir des choses inquiétantes à l’horizon. Je suis tombé sur du code, des concepts et des termes complètement opaques. Je me suis pas inquiété tout de suite. C’était assez lointain de ma zone d’intervention.

J’aurais dû m’inquiéter tout de suite.





Quelques semaines plus tard, ma première grande mission dans l’équipe tombait sur mon bureau.

La réécriture entière d’un service charnière pour le produit.

Sans rentrer dans les détails, on peut comparer ce service à une sorte de CDN. Le client envoie un fichier ZIP, mon service doit gérer beaucoup de choses. Extraction à la volée des fichiers avec récursion (zip dans zip), upload, caching, service des fichiers statics, versioning, metadata. Le tout en gardant 100% des calls en dessous de 200ms.

Faire correctement ce genre de choses nécessite une compréhension interne des fonctionnements de Javascript. Je le savais pas encore. J’étais sur le point de souffrir devant des erreurs et des comportements incompréhensibles.

Le piège Javascript venait de se refermer sur moi.





En surface, Javascript est très accessible et tu fais vite des merveilles avec. Une connaissance superficielle des mécaniques internes est souvent suffisante. D’où le nombre de personnes qui l’utilisent sans vraiment savoir ce qu’ils font.

Mais quand tu finis par t’attaquer à des choses plus complexes, t’es vite perdu, et ton syndrome de l’imposteur commence à te fixer intensément.



Variables inconnues

Avant de te parler de ce qui m’avait rendu fou lors de la création de ce service, revenons quelques années en arrière. Comme beaucoup de gens, j’ai appris Javascript sur le tas. Il fallait en faire. J’ai commencé à en faire.

Comme l’oblige l’époque, je fais du jQuery. Je pense que je suis un dieu dessus. J’arrive à faire tout ce qu’on me demande. Mais malgré ce que je pense, de temps en temps, je me prends une énorme claque dans la gueule.

Des choses simples ne marchent pas. Ça bug sans raison. Et bizarrement, le fait que je donne des grands coups sur mon clavier ne résout pas le problème.

Mes problèmes venaient de la première chose que j’avais pas comprise avec Javascript : le fonctionnement interne des variables et des types.

Pour comprendre de quoi je te parle, regardons un peu de code.

Que va afficher ce code et surtout pourquoi ?


const originalEzio = {
  "name": "ezio Auditore da Firenze",
  "weapon": "Hidden Blade",
  "metadata": {
    "version": "Original",
    "type": "Assassin"
  }
};

originalEzio.name[0] = 'E';

function getHeroCopy(originalHero) {
  const copyHero = {
    name: originalHero.name,
    weapon: originalHero.weapon,
    metadata: originalHero.metadata
  };

  copyHero.metadata.version = 'Copy';

  return copyHero;
}

const copyOfEzio = getHeroCopy(originalEzio);

console.log('Original : ', originalEzio);
console.log('Copy : ', copyOfEzio);

Alors oui, je sais. Ça fait test d’entretien Javascript à la con mon truc. Mais prends le temps de prédire ce que ça va afficher juste pour voir. C’est bon ?

Vérifions ta prédiction en appuyant sur play du Repl juste en dessous.





Et oui, l’objet original a été changé ! Si tu ne sais pas l’expliquer clairement, c’est qu’il te manque des choses dans les fondations du langage. Petite explication en quelques mots.

Les variables sont divisées en deux grandes catégories : les primitives et les complexes.

  • Les primitives (string, number, boolean, …) pointent vers des valeurs uniques.

Elles sont immutables. D’où le fait que la string ne change pas (ligne 10). D’ailleurs, si tu rajoutes « use strict » au début du fichier, ça throw immédiatement. En monde strict, Javascript ne permet pas cette satanerie.

  • Les complexes (object, …) pointent vers des références de valeurs.

Elles sont mutables. Ligne 16, je fais référence à l’objet metadata du héro original et je l’assigne à la metadata de la copie. En changeant la copie, je change donc la référence de l’original.

Quand j’ai commencé, je n’avais pas ces notions. Et crois-moi, ce n’est pas drôle de ne pas les avoir. Énormément de monde ne les ont pas.





Le but aujourd’hui, c’est pas de te faire un cour. Le but c’est de pointer du doigt les pièges que j’ai rencontrés. Faire en sorte que toi, tu les évites.

J’ai une recommandation pour toi en fin d’article pour comprendre et dominer tous ces pièges d’un coup.

Mais avant ça, continuons à désigner les endroits où je me suis vautré en mode video gag.



What the fuck is this

Mon clavier a encore pris très cher quand je suis tombé sur la gestion des contextes.

Pour la réécriture du service, j’étais aidé par de nombreuses librairies internes et externes. Certaines plus récentes que d’autres. Certaines mieux faites que d’autres. Elles utilisaient toute la dimension objet de Javascript.

Ou plus précisément, la programmation orientée prototype, une forme incomplète de programmation objet.

Même aujourd’hui, malgré le sucre syntaxique des classes, c’est toujours des prototypes. On veut te faire croire des choses. Javascript n’est pas vraiment un langage-objet. Rendez-vous sur twitter pour la bagarre pour ceux et celles qui ne sont pas d’accord.



// ce que tu utilises
class Assassin {
  constructor(name) {
    this.name = name;
  }
  
  getCreed() {
    return "Nothing is true, everything is permitted.";
  }
}

//---------------

// ce que fait vraiment JS derrière
function Assassin(name){
  this.name = name;
}

Assassin.prototype.getCreed = function() {
  return "Nothing is true, everything is permitted.";
}


Bref, j’ai fait la connaissance des contextes en Javascript. Avec ces règles de périmètre schizophrènes. J’ai immédiatement commencé à utiliser ma tête pour taper sur mon clavier.

Je te fais chier encore cher lecteur, une autre petite question d’entretien Javascript à la con. Je suis un gros lourd. Mais c’est une erreur vicieuse à bien comprendre.

Que va afficher ce code et surtout pourquoi ?


const altair = {
  name: "Altaïr Ibn-La'Ahad",
  templarsKilled: ['Tamir', 'Talal', 'Sibrand'],
  showTemplarsKilled: function() {
    console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)

    this.templarsKilled.forEach(function(templarKilled) {
      console.log(`${this.name} killed ${templarKilled}`)
    });
  }
};

altair.showTemplarsKilled();



Rebelote, tu peux vérifier ta prédiction avec le Repl juste en dessous.





Pourquoi le second log (ligne 8) ne fonctionne pas ? Pourquoi le premier log (ligne 5) fonctionne ? Pourquoi utiliser une fonction fléchée (ligne 7) résout le problème ?

Si tu n’es pas capable de répondre à ces questions, c’est que le fameux contexte (this) Javascript est flou pour toi. Et c’est compréhensible. En javascript, le contexte ne se comporte pas du tout comme dans les autres langages.

On a affaire à un monstre.





En théorie, « this » représente le contexte de la fonction. Un objet associé à l’invocation de la fonction. Sauf que c’est pas si simple. En vérité, il va être déterminé selon comment il est appelé.

Regardons quelques exemples.

Appeler dans une fonction, le contexte va être l’objet global. Si tu le sais pas, tu modifies tragiquement l’objet global. C’est le mal.

this.creed = "Nothing is true, everything is permitted.";

function showCreed() {
    console.log(this.creed)
}

showCreed();

Sauf en strict mode. En strict mode c’est undefined. Tu le sais pas, cette fois y’a tout qui plante.

"use strict"

this.creed = "Nothing is true, everything is permitted.";

function showCreed() {
    console.log(this)
}

showCreed(); // undefined

Appeler en méthode d’une fonction, le contexte va être l’objet en question, comme on le veut. C’est pour cette raison que la fonction « showTemplarsKilled » plus haut fonctionne. Mais pas la fonction imbriquée suivante. La suivante, elle a son propre contexte.

...
showTemplarsKilled: function() {
    // this -> objet context
    console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)

    this.templarsKilled.forEach(function(templarKilled) {
      // this -> function context
      console.log(`${this.name} killed ${templarKilled}`)
    });
}
...

Je sais pas si tu as déjà vu du code créer des variables genre « self » ou « _this » qui passait le contexte courant ? C’est exactement pour cette raison. Un hack relativement dégueulasse pour garder le contexte courant.

...
showTemplarsKilled: function() {
    const self = this;
    console.log(`List of templar killed (${self.templarsKilled.length}) by ${self.name}`)

    self.templarsKilled.forEach(function(templarKilled) {
      console.log(`${self.name} killed ${templarKilled}`)
    });
  }
...

Aujourd’hui, la manière la plus élégante est d’utiliser une fonction arrow. En plus de rendre notre code plus lisible et plus court, elle passe le contexte courant à la fonction appelée. Propre.

...
showTemplarsKilled: function() {
    console.log(`List of templar killed (${this.templarsKilled.length}) by ${this.name}`)

    this.templarsKilled.forEach(templarKilled => console.log(`${this.name} killed ${templarKilled}`));
  }
...

Bref, je te dis que je veux pas faire un cour, mais je me lance dans des explications quand même. Arrête-moi quand je commence à partir dans tous les sens comme ça.

En tout cas, pendant que je faisais ce fameux service, j’étais loin de me douter de tout ça. Et toutes ces règles de contexte selon l’endroit et la façon dont tu appelles m’ont fait péter un plomb.

Ça rendait la vitesse et la qualité de ce que je produisais … disons discutable. Les premières semaines dessus ont été laborieuses. Et même si c’était faux, j’avais l’impression que mon équipe commençait à douter de ce que je pouvais apporter.





Avec beaucoup (trop) de temps et de douleur, j’arrivais progressivement, module par module, à produire quelque chose. Pourtant, c’était juste le début de mes découvertes. J’étais pas au bout de mes peines.



Déploiement

Je te passe les diverses aventures sur la route, passons directement au moment du déploiement. À ce moment-là je suis persuadé que mon affaire fonctionne. J’ai 3 millions de tests. Ça tourne en dev depuis une semaine. J’aurais volontiers parié un bras et deux jambes.

Lundi matin, je déploie finalement le service, ça fonctionne parfaitement.

Mais plus la journée passait, plus les utilisateurs utilisaient progressivement la nouvelle version, plus je voyais le temps de réponse augmenter de façon inquiétante. Au milieu de l’après-midi, le premier mail d’un client arrive dans ma boite.

C’est clairement lié à mon service.

Sauf que, même en regardant précisément le code lent à répondre, je ne comprenais pas. Les temps de réponse continuaient à s’allonger. J’étais de plus en plus dans le brouillard.





C’était pas une grosse erreur. C’était une collection de petites erreurs subtiles qui ralentissaient mon application. Regardons de plus près l’une d’entre elles. Promis, dernière question d’entretien, après je te laisse tranquille.

Quel est le problème avec le code suivant ?


...
function _load (assetFile, assetRoute) {
  return this.cdn.getFileInfo(assetFile)

  .then(assetInfo => this.setAssetInCache(JSON.Stringify(assetFile), assetInfo))

  .then(() => this.getAssetFromCache(assetRoute))

  .then(data => {
    if (data) {
      return Promise.resolve(data)
    } else {
      return Promise.reject("Can't get asset from cache.")
    }
  })

  .catch(error => Promise.reject(error))
}
...

Le problème se situe ligne 5 avec l’utilisation de JSON.stringify. C’est une opération bloquante. Dans un monde asynchrone non bloquant, il faut faire très attention à ce genre de chose.

JSON.stringify bloque le thread dans lequel il se trouve. Javascript étant single thread, c’est problématique. Alors oui, la promesse donne un délai au blocage. Mais quand le stringify s’exécute, rien ne s’exécute tant que c’est pas fini.

Bloquant ainsi tout le reste de l’application.

La plupart du temps, stringify n’est pas un problème. Ce qui doit être stringifié est tellement petit que la fonction se fait quasi instantanément. Sauf qu’ici, c’est des milliers de fichiers -plus ou moins gros- de façon simultanée qui sont traités.

Milliseconde par milliseconde, le temps de réponse montait jusqu’à 1 seconde par appel !

Plus les utilisateurs utilisaient l’application, plus c’était un calvaire pour tout le monde.





C’est le jour où j’ai vraiment commencé à m’intéresser à l’event loop.

Son fonctionnement, ces enjeux, les différentes phases. Des timers, au close callback en passant par le I/O polling. Ça allait me servir beaucoup sur NodeJS. Mais aussi sur javascript de façon générale dans le navigateur.

Alors, il faut savoir que même si le fonctionnement global de l’event loop dans le navigateur et dans NodeJS est le même, il existe des différences quand on zoom. Je le dis car tu auras toujours un « expert » autoproclamé pour te corriger -de façon insupportable- comme si c’était important.

Bref, avec un peu de temps et en pleurant un peu de sang, j’ai fini par corriger tous les endroits incriminés. Le temps de réponse est passé sous la barre des 200 ms. Et je pensais que j’en avais fini d’apprendre à la dure.



Point de rupture

Quelques semaines plus tard, je participais à une réunion avec mes collègues. Une réunion importante où on allait discuter technique. Un nouveau service était en réflexion et l’idée est de trouver des solutions en amont.

Cette réunion allait être le point de rupture qui me poussera vraiment à agir.

J’ai pratiquement pas parlé de la réunion. Malgré mes apprentissages sur le service, c’était pas assez pour suivre. Des concepts et des termes techniques fusaient dans tous les sens.

Suivre la discussion devenait de plus en plus compliqué. Y participer sans dire de bêtise, encore plus. Ça parlait de closures, de générateurs, de risque de memory leak et d’utiliser des proxies pour faire du monitoring poussé.

Rien de tout ça était clair dans ma tête. Il était temps d’agir pour sortir de ce brouillard.





Élever son jeu

En revenant à mon poste après la réunion, j’ai pris mon courage à deux mains. J’ai demandé à un de mes collègues des précisions sur le contenu de la réunion. La discussion s’est très vite orientée sur un bouquin qu’il avait lu et qu’il me conseillait chaudement.

Ma recommandation du jour : Secrets of the Javascript Ninja.

Ce livre est le point de départ de toute ma confiance avec Javascript.

En m’expliquant profondément les fonctionnements internes, les comportements à la surface sont devenus limpides. Mon code est devenu rapide et robuste. Les questions pièges d’entretien, faciles.

Il commence tout doux avec ce qui se passe dans le navigateur avec Javascript. Puis, il rentre vite dans le vif du sujet avec les fonctions. Comprendre -vraiment- comme elles fonctionnent, ça change tout.

Puis l’incroyable partie sur les closures et le fonctionnement des champs lexicaux qui a été une révélation pour moi.

Ensuite, le fonctionnement des générateurs, des promesses et des prototypes. Enfin, ça finit par une deep dive dans la sacré sainte event loop que j’ai ENFIN pu comprendre. Je suis sorti de ce bouquin avec une vision claire. Prêt à en découdre.





Alors, soyons clairs. J’ai toujours été très honnête avec mes recommandations. Ce livre n’est pas une lecture facile.

Ça ne s’adresse pas à toi si tu commences à peine le Javascript. Il y a des moments complexes où j’ai dû bien réfléchir, lire, relire et regarder les schémas pour bien percuter. Mais c’est bien toute la valeur de ce livre.

Ce livre s’adresse à ceux qui font déjà du Javascript depuis quelque temps et qui veulent élever leur jeu. Il s’adresse à ceux qui veulent dominer ce langage. Il s’adresse à ceux qui veulent se créer une expertise.

Si c’était si simple que ça, tout le monde serait expert. Ce bouquin te pousse dans le brouillard pour mieux t’en faire sortir. Il n’y a pas d’évolution sans friction.



Épilogue

Comme beaucoup de monde, je suis tombé dans le piège Javascript en pensant que c’était un langage « facile ». Toutes mes erreurs et mes moments douloureux auraient pu être évités en prenant au sérieux l’apprentissage du langage en amont.

Qui me parle ?

jesuisundev
Je suis un dev. En ce moment, je suis développeur backend senior / DevOps à Montréal pour un géant du jeux vidéo. Le dev est l'une de mes passions et j'écris comme je parle. Je continue à te parler quotidiennement sur mon Twitter. Tu peux m'insulter à cet e-mail ou le faire directement dans les commentaires juste en dessous. Y'a même une newsletter !

Pour me soutenir, la boutique officielle est disponible ! Sinon désactiver le bloqueur de pub et/ou utiliser les liens affiliés dans les articles, ça m'aide aussi.

23 commentaires sur “Javascript : ce que j’avais pas compris”

  1. Merci pour l’article! Effectivement la notion de pointeur avec les objets a été un bon casse-tête pour beaucoup de personnes x)
    Connais-tu un livre comme celui que tu conseilles mais en Français? Ça serait intéressant!

      1. Oui, très pénible cette disparition des bouquins en français. Moi je suis un peu « old school » et j’aime les livres. Jai un « javascript : la référence » de David Flannagan, qui a été un prodigieux compagnon, mais c’était il y a plus de dix ans. Et aujourd’hui je retrouve rien de comparable en français 🙁

    1. Peut-être créer une fonction asynchrone avec les appels à stringify puis à setAssetInCache ?
      Moi aussi je veux savoir, je suis curieux ! 😀

      1. Clairement, je me posais la même question ! Une rapide recherche m’a mené du côté des web workers mais je suis sûr qu’une solution plus simple existait.

    2. Salut !

      Il y a plusieurs solutions.

      Celle que j’avais utilisée à l’époque c’était de le traiter chunk par chunk en streamant mon objet. Comme la data est découpée en petits morceaux, l’application peut faire autre chose entre le traitement de deux morceaux, rien ne bloque. Je l’avais codé à la main, mais tu as des librairies qui font ça comme : https://www.npmjs.com/package/JSONStream#jsonstreamstringifyopen-sep-close

      Une autre solution est de passer par les worker thread. Tu délègues un thread au traitement bloquant pendant que ton thread principal reste disponible pour le reste. J’ai dédié un article au Worker Threads ici : https://www.jesuisundev.com/comprendre-les-worker-threads-de-nodejs/

  2. La question a 1 million de dollars:
    en tant que dev php depuis très (trop?) longtemps, a chaque fois que je me frotte a Javascript, je tombe très (trop?) rapidement dans les mêmes pièges, dont certains décrits ci dessus. J’imagine que tous les devs qui arrivent chez JS en provenance de C, Java, Python et co. ont des difficultés semblables.
    Alors, est-ce vraiment raisonnable de vouloir passer a JS?
    Je suis sur qu’il y a un million de bonnes raisons pour le faire, mais mon impression reste celle d’un langage mal dessiné a la base, avec des moteurs rapides et du io async certes, mais avec beaucoup trop de pièges des l’entrée. Déjà il y a trop de modes de déclarer une variable… Foreach vs. this… le scoping global des variables par défaut… les trente-deux façons de déclarer une classe rajoutées par es6… le mode normal vs strict vs typescript… etc etc
    Peut être des langages comme Rust ou Go sont tout aussi performants, mais moins propices au foot shooting ?

      1. Typescript est un langage intéressant, le typage permet d’éliminer des erreurs potentielles à la compilation (ça aide à réduire la boucle de feedback).
        Par contre ça reste du Javascript après compilation. Donc c’est quand même important de bien connaître le Javascript avant de se lancer sur du Typescript je pense.
        Se lancer « juste » sur du Typescript sans creuser peut amener aux même type d’erreurs que tu décris dans le post 🙂

  3. Si vous voulez quelque chose de plus simple et moins long a lire que un livre vous avez aussi la série de mail de Dan Abramov qui permettent de développer un mental model autour de ce langage, très utile pour les questions pièges lors des entretients (C’est gratuit !).
    Le lien : https://justjavascript.com/

  4. Super article, merci beaucoup !
    Je viens du monde Java, et de ma lorgnette, ces « pièges » semblent particulièrement tordus.
    J’imagine qu’il faut suffisamment de pratique et un oeil avisé pour détecter ces problèmes.

    Question : puisque « Secrets of the Javascript Ninja » est conseillé aux développeurs avec déjà un petit bagage JS, quel livre ou cours conseillerais-tu pour un néophyte ?

  5. Super article. Je me sens totalement concerné par cette connaissance approximative du JS.
    J’ai commencé « Secrets of the JavaScript ninja » et en seulement 50 pages, ça m’a déjà éclairci pas mal de mystères.

  6. Bonjour, petite rectification, pour la fonction fléchée, elle ne passe pas le contexte justement, elle n’a tout simplement pas de this donc même avec une arrow function on peut perdre le contexte de this. J’utilise le bind pour être sûr de toujours avoir le bon contexte. La doc de MDN explique très bien cela ou alors je te conseille de voir les billets de Christophe Porteneuve (je crois que c’est un des contrib d’ecmascript) Sinon très bon article, personnellement j’adore ce langage mais c’est vrai que parfois il rend fou.

    1. La fonction fléché passe le contexte de son parent. Elle n’a pas de contexte mais je vois pas le rapport.
      La source dont tu parles te contredit : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Functions/Arrow_functions

      « Les fonctions fléchées ne créent pas de nouveau contexte, elles utilisent la valeur this de leur contexte. Aussi, si le mot-clé this est utilisé dans le corps de la fonction, le moteur recherchera la référence à cette valeur dans une portée parente. »

      1. C’est exactement ce que je voulais dire, de ce que j’ai compris elle ne transmet pas le contexte courant vus qu’elle n’a pas de contexte de base, elle transmet le contexte parent et celui-ci peut être n’importe quoi, d’où l’utilité du bind. C’est pour cela que l’on n’utilise pas d’arrow function dans un objet. L’arrow function est très utile pour une callback style foreach, map ou addeventlistener. Pour le reste je ne suis pas expert, si je dis des bêtises je serais ravis d’avoir une explication car je commence a douter maintenant^^

T'en penses quoi ?

Your email address will not be published. Required fields are marked *