Les cinq règles d'or du JavaScript moderne

Ces règles sont tirées de la préface du livre Modern JavaScript for the Impatient de Cay Horstmann :

  1. Déclarer les variables avec let ou const, pas avec var
  2. Utiliser le mode strict
  3. Connaître les types et éviter la conversion automatique
  4. Comprendre les prototypes, mais utiliser la syntaxe moderne pour les classes
  5. Ne pas utiliser this en dehors des constructeurs ou des méthodes

Je trouve qu'elles font preuve de bon sens pour éviter les pièges de JavaScript.

Je les détaille dans ce billet en les croisant avec d'autres sources.

Règle 1 - Utiliser let ou const, pas var

let et const ont été introduits par ES2015 (ES6) car var est source de confusion et de bogues.

La bonne pratique est de ne jamais utiliser var, même si un bon développeur peut toujours lui trouver des cas d'utilisation valides.

Hissage (hoisting)

Voici un exemple un peu déroutant avec var :

var x = 5

function doStuff(isBig) {
    if (isBig) {
        var x = 10
        return x
    }
    return x
}

doStuff(false)  // undefined

doStuff() retourne undefined à cause du hissage (hoisting).

L'interpréteur JavaScript traite le code en deux "phases" :

  1. une phase d'analyse et de compilation
  2. une phase d'exécution du code

Le mot "hissage" est une métaphore qui décrit la première phase dans laquelle chaque déclaration semble hissée au sommet de sa portée.

Donc, pour comprendre pourquoi doStuff() retourne undefined ci-dessus, il faut comprendre la portée de var et imaginer la phase de hissage :

var x = 5

function doStuff(isBig) {
    var x  // Hoisted!
    if (isBig) {
        x = 10
        return x
    }
    return x
}

doStuff(false)  // undefined

Trois règles pour ne pas se faire mordre par le hissage :

  1. ne pas utiliser var
  2. utiliser le mode strict (voir règle 2)
  3. déclarer les variables, fonctions et classes avant de les utiliser

Brendan Eich explique pourquoi JavaScript hisse les variables :

var hoisting was thus unintended consequence of function hoisting, no block scope, JS as a 1995 rush job. ES6 'let' may help.

Portée

var permet de déclarer une variable dont la portée dépend de son contexte d'exécution :

  • portée locale quand elle est déclarée à l'intérieur d'une fonction (sa portée se limite alors à cette fonction)
  • portée globale quand elle est déclarée en dehors d'une fonction

let et const permettent de déclarer des variables dont la portée est celle du bloc courant.

In ancient times, JavaScript programmers used "immediately invoked functions" to limit the scope of var declarations and functions.

Propriété de l'objet global

Quand une variable déclarée avec var est globale, elle devient automatiquement une propriété de l'objet global (window dans le navigateur) :

var foo = 1
window.foo  // 1

Ce qui peut devenir problématique :

window.console.log('toto')  // toto
var console = 'foo'
window.console.log('toto')  // TypeError: console.log is not a function.

let n'affecte pas l'objet global :

let bar = 2
window.bar  // undefined

Redéclaration

Avec var il est possible de redéclarer :

var foo = 1
var foo = 2

Pas avec let :

let bar = 1
let bar = 2  // Uncaught SyntaxError: Identifier 'bar' has already been declared

Variables non initialisées et zone morte temporaire

On a vu que pendant le hissage, les déclarations de variables semblent hissées au sommet de leurs portées.

On appelle "zone morte temporaire" (Temporal Dead Zone ou TDZ) la période pendant laquelle une variable est déclarée mais pas encore initialisée.

Si on accède à une variable qui se trouve en zone morte temporaire :

  • avec var, la valeur est undefined
  • avec let et const, une exception ReferenceError est levée
let baz = () => {
    console.log(bar) // undefined
    console.log(foo) // ReferenceError: Cannot access 'foo' before initialization
    var bar = 1
    let foo = 2
}
baz()

Règle 2 - Utiliser le mode strict

Le mode strict est arrivé avec ECMAScript 5 en 2009 dans le but de fixer l'enfer de la programmation ES3.

Mark S. Miller déclare à cette occasion :

I would say in fact that ECMAScript 5 strict mode has crossed the threshold into actually being good :)

Le mode strict n'est pas activé par défaut.

On y souscrit via une instruction pragma qui prend la forme d'une string afin d'assurer la rétrocompatibilité (elle est analysée comme une no-op par ES3) :

"use strict"

On peut souscrire au mode strict soit au niveau du script, soit au niveau des fonctions.

Instruction pragma ou pas, on est toujours en mode strict dans :

Pour souscrire au mode strict dans le REPL de Node.js :

node --use-strict

Le bénéfice majeur du mode strict est de garantir une portée statique, c'est à dire de pouvoir déterminer la portée d'une variable en fonction de sa position dans le code !

Ressources :

Règle 3 - connaître les types et éviter la conversion automatique

J'ai déjà rassemblé des notes sur les types et la conversion en JavaScript.

Ce qu'il faut savoir c'est qu'en JavaScript, il y a beaucoup de conversions implicites de types de données. On parle de coercition (coercion).

Par exemple, l'expression x + y devient :

  • un nombre si les deux opérandes sont des nombres
  • une chaîne de caractères si au moins un des opérandes est une chaîne de caractères

Dans les autres cas, les règles de coercition deviennent complexes et les résultats sont rarement utiles.

Brendan Eich nous explique l'origine de ce comportement :

Insane type coercions came after the ten day sprint (where == said false if types did not match), based on early inside-Netscape user requests to, e.g., make string-type HTTP status property containing "404" == 404. My fault for responding to them.

Lesson for designers: say no!

La bonne pratique est de toujours utiliser des conversions explicites (type casting).

À noter : l'Unicode de JavaScript utilise UTF-16 !

Ressources :

Règle 4 - Comprendre les prototypes, mais utiliser la syntaxe moderne des classes

Le système d'héritage prototypal de JavaScript n'est pas évident à comprendre.

Cette difficulté de compréhension résulte en partie de la similitude des termes : le mot prototype, la propriété interne [[Prototype]] et la propriété prototype des fonctions.

Chaîne des prototypes

En JavaScript, tout objet pointe vers un prototype.

Un prototype est simplement un autre objet qui regroupe des propriétés communes à plusieurs objets.

Le prototype d'un objet donné est référencé par une propriété interne appelée [[Prototype]] manipulable via :

  • Object.getPrototypeOf()
  • Object.setPrototypeOf()
  • Object.create()
  • etc.

Quand on crée un objet, son [[Prototype]] pointe automatiquement vers l'objet Object.prototype.

Quand on crée une fonction, son [[Prototype]] pointe automatiquement vers l'objet Function.prototype dont le [[Prototype]] pointe vers Object.prototype.

Des chaînes de prototypes sont donc créées :

76eec93502546c008347105ba5dd0ca1

Quand on tente d'accéder à l'attribut d'un objet, si aucun résultat n'est trouvé, une recherche est effectuée dans sa chaîne des prototypes.

Cette recherche ne fonctionne qu'en lecture. L'écriture d'une propriété sur un objet est toujours effectuée sur l'objet.

Héritage et classes

JavaScript n'a pas vraiment de système de classe.

À la place, on utilise l'opérateur new avec des fonctions qu'on appelle alors "fonctions constructrices".

La syntaxe moderne des classes est un sucre syntaxique permettant de créer des fonctions constructrices.

En plus d'une propriété interne [[Prototype]], toute fonction reçoit automatiquement trois autres propriétés :

  • name
  • length
  • prototype

La propriété prototype d'une fonction constructrice pointe vers un objet créé automatiquement dans lequel on peut stocker des méthodes et des propriétés qui seront partagées par toutes ses instances.

Toute instance de classe créée avec new voit sa propriété interne [[Prototype]] pointer vers l'objet référencé par la propriété prototype de sa fonction constructrice.

En d'autres termes, l'objet référencé par la propriété prototype de la fonction constructrice est partagé entre toutes les instances.

La syntaxe extends des classes modernes s'occupe de construire automatiquement la chaîne des prototypes. Avant, il fallait le faire manuellement.

Ressources :

Règle 5 - Ne pas utiliser this en dehors des constructeurs ou des méthodes

En JavaScript, un mot clé this est automatiquement défini dans la portée de chaque fonction.

Deux significations de this sont souvent supposées, mais fausses :

  • this n'est pas une référence à la fonction elle-même
  • this n'est pas une référence à la portée de la fonction

this est en fait une liaison dynamique (runtime binding) effectuée au moment où une fonction est invoquée.

Ce à quoi this fait référence est entièrement déterminé par l'endroit où la fonction est invoquée (son site d'appel), et pas par l'endroit où elle est déclarée.

Pour déterminer ce à quoi this fait référence, il faut trouver le site d'appel direct de la fonction concernée et appliquer quatre règles, dans cet ordre :

  1. Invoquée avec new ? this fait référence à l'instance nouvellement créée
  2. Invoquée avec call, apply ou bind ? this fait référence à l'objet spécifié
  3. Invoquée en tant que méthode d'un objet ? this fait référence à l'objet possédant la méthode invoquée (par exemple obj.foo())
  4. Par défaut : undefined en strict mode, l'objet global sinon (par exemple foo())

À noter :

  • si vous appelez une fonction constructrice sans new, this est undefined
  • dans une fonction fléchée, this est lié à ce que this signifie dans le contexte englobant de la fonction fléchée

Pour conserver votre santé mentale, le conseil de Cay Horstmann est de ne pas utiliser this en dehors des constructeurs ou des méthodes.

Ressources :

Avant La description de Pull Request parfaite Après Prendre des notes intelligentes dans Obsidian

Tag Kemar Joint