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.

clearfix