JS Attitude

« Je n’ai jamais rencontré un expert JavaScript au chômage »

Le mode strict de ES5+

| Commentaires

La version 5 d’ECMAScript, le standard sur lequel se base JavaScript, a introduit un mode d’interprétation particulier du code, appelé le mode strict. Le mode traditionnel est qualifié de « mode négligé » (sloppy mode).

Ce mode s’active au travers d’un marqueur particulier dans nos codes, ignoré par les runtimes antérieures. En plaçant nos codes dans ce mode d’interprétation, nous avons la possibilité de découvrir plus vite, plus tôt, des sources d’erreurs potentielles.

Cet article décrit comment activer ce mode, et quelles en sont les conséquences précises sur l’interprétation de notre code JavaScript.

Activer le mode strict

Le mode strict s’active en ajoutant une expression String spécifique au code, obligatoirement avant toute autre instruction ou expression (mais des commentaires peuvent la précéder) :

1
'use strict';

(Suivant votre style dominant, vous pouvez utiliser des apostrophes ' ou guillemets ").

Le mode s’applique à la portée courante. On a donc trois cas de figure :

  • Le fichier complet. C’est dangereux si ce fichier n’est pas enrobé en tant que module par votre système de build, car alors vous activez le mode strict au niveau global, ce qui a de fortes chances de casser des scripts tiers, bibliothèques, etc.
  • Le « script complet », c’est-à-dire le code passé à eval, à new Function, à un gestionnaire d’événement en attribut inline (beuark !) ou à setTimeout (mais préférez une référence de fonction, quand même). Le mode strict qui figurerait dans un tel script est alors limité au script lui-même.
  • La fonction courante. C’est le cas dans une fonction explicite (par exemple une IIFE ou une fonction classique) ou si votre fichier est enrobé en tant que module automatiquement (par exemple dans un contexte CommonJS ou module ES6).

Un impact extrêmement important de cette approche est qu’elle permet à des scripts non-stricts et à des scripts stricts de coexister dans la même page, chacun avec sa sémantique d’analyse et d’exécution. Il est ainsi possible de migrer incrémentalement votre base de code vers du strict, et de continuer à utiliser des scripts tiers pas encore stricts.

Histoire que ça passe pour tout le monde, dans nos exemples de code pour cet article, on enrobera systématiquement l’ensemble par une IIFE :

1
2
3
4
5
(function() {
  'use strict';

  // Nos exemples d'impact ici
})();

Et si mon script ne passe pas ?

C’est là tout l’intérêt du truc : si votre code ne satisfait pas le mode strict que vous lui avez donné, la runtime JS va tout simplement refuser de l’exécuter. Elle ne se contente pas d’avertissements, elle fait péter une erreur spécifique, histoire de comprendre ce qui se passe. Toute la portée concernée refuse donc de s’exécuter.

Du coup, ça vous pète à la gueule en développement, bien visible, ce qui est exactement le but recherché : vous voulez identifier le problème le plus tôt possible, pas un de ces jours alors que c’est déjà déployé en production.

Moins d’erreurs sournoises

Un premier volet du mode strict consiste à hurler sur des sources usuelles d’erreurs plutôt fourbes.

Litéraux octaux « à l’ancienne »

La plupart des runtimes JavaScript autorisaient les litéraux octaux « façon C » : si c’était préfixé par un zéro, c’était de l’octal. Au moins, c’était cohérent avec le comportement pourri de parseInt :

1
2
3
// Sloppy mode

console.log(042); // => 34

C’est d’autant plus débile que depuis ES3, soit tout de même le siècle dernier (1999), les litéraux octaux ne sont plus obligatoires dans les runtimes, ils sont juste là, si l’implémenteur le souhaite, par compatibilité avec ES1 et ES2… (voir ES-262-3, annexe B, section B.1.1).

Vous me direz que quand on tape un litéral pareil on sait ce qu’on fait, mais en fait on trouve souvent des préfixes zéros pour homogénéiser / aligner les valeurs, genre :

1
2
3
4
5
6
7
8
// Sloppy mode

var flags =
  016 + // Enable discrete sampling
  128 + // Enable PCM compression
  256;  // Enable streaming frames

flags   // => 398 (au lieu de 400)

En mode strict, c’est fini :

1
2
3
4
5
6
7
(function() {
  'use strict';

  console.log(042);
})();

// => SyntaxError: Octal literals are not allowed in strict mode.

Notez qu’avec ES6, on récupère des litéraux octaux explicites (0o42), et tant qu’à faire des litéraux binaires, que j’ai toujours adorés en Ruby (0b100010). Elle est pas belle la vie ?

Fuite de globales

C’est une des vraies verrues de JavaScript : si on oublie le var, paf la globale. C’est juste trop moche, comme fonctionnement… Évidemment, les linters type JSHint vont chopper ça tout de suite (réglage undef), mais ça reste un avertissement de linting, rien qui vous saute à la gueule à coup sûr…

Démonstration :

1
2
3
4
5
6
7
8
9
// Sloppy mode

(function() {
  var x = y = 42;

  dommage = 'Éliane';
})();

console.log(y, dommage); // => 42 Éliane

Le mode strict va te taper sur les doigts quand tu fais une bourde pareille :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function() {
  'use strict';

  var x = y = 42;
})();

// => ReferenceError: y is not defined

(function() {
  'use strict';

  dommage = 'Éliane';
})();

// => ReferenceError: dommage is not defined

À mort les globales ! (Surtout quand on ne fait pas exprès…)

Échecs silencieux d’écriture ou suppression de propriétés

Autre source de bugs bien fourbes et sournois : les opérations sur objets qui échouent silencieusement. Par exemple, l’écriture sur une propriété en lecture seule, ou la suppression d’une propriété non configurable (donc non supprimable).

Par exemple :

1
2
3
4
5
6
7
// Sloppy mode

var text = 'hello';
text.length = 3; // => 3
text.length      // => 5

delete Math.PI;  // => false

Il est également possible de supprimer des références simples (non qualifiées), ce qui est plus souvent dangereux qu’utile :

1
2
3
// Sloppy mode

delete navigator; // => true (et meeeeerde…)

Mais en mode strict, pas moyen d’échouer en douce :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function() {
  'use strict';

  var text = 'hello';
  text.length = 3;
})();

// => TypeError: Cannot assign to read only property 'length' of hello

(function() {
  'use strict';

  delete Math.PI;
})();

// => TypeError: Cannot delete property 'PI' of #<Object>

…ni de supprimer des références simples :

1
2
3
4
5
6
7
(function() {
  'use strict';

  delete navigator;
})();

// => SyntaxError: Delete of an unqualified identifier in strict mode.

Voilà qui est plus conforme à l’excellent principe de fail-fast.

this global

En JavaScript, les méthodes n’appartiennent pas, implicitement, à quelque objet que ce soit : on a que des fonctions, en fait. Le fait que certains objets ou prototypes de constructeurs référencent des fonctions n’a pas d’incidence sur celles-ci.

Du coup, à part un cas précis d’appel, ou suite à un binding explicite, il arrive régulièrement qu’on se prenne les pieds dans le tapis en appelant dans un contexte global une fonction conçue comme méthode, qui essaie donc de manipuler son this. Et là, c’est le drame : this est l’objet global (dans un navigateur, ce sera window, dans Node ce sera global).

(Si à ce stade vous vous dites « what the fuck!? », prenez 30 minutes pour regarder cette vidéo.)

Beaucoup de gens se font avoir par ça, car lorsque la méthode se contente de lire ou écrire une propriété sur this, ça nous fait un bel échec silencieux :

1
2
3
4
5
6
7
8
9
10
11
12
13
// Sloppy mode

var obj = {
  name: 'Georges Abitbol',
  rebrand: function rebrand() {
    this.name = "L’homme le plus classe du monde™";
  }
};

var fx = obj.rebrand;
fx();
obj.name; // => 'George Abitbol'
name      // => 'L’homme le plus classe du monde™'

Mais si on est en mode strict, this ne sera pas l’objet global : il sera carrément undefined ! Ça va bien faire péter nos accès direct cash ça Madame :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function() {
  'use strict';

  var obj = {
    name: 'Georges Abitbol',
    rebrand: function rebrand() {
      this.name = "L’homme le plus classe du monde™";
    }
  };

  var fx = obj.rebrand;
  fx();
})();

// => TypeError: Cannot set property 'name' of undefined

Pour aller vraiment au fond des choses, même si ça n’a rien à voir avec la disparition du this global, j’en profite pour préciser qu’en mode strict this n’est pas boxed, c’est-à-dire que ce n’est pas forcément un objet : si la fonction est appelée avec un this primitif (litéral numérique, chaîne de caractères ou booléen), celui-ci restera primitif, et son typeof va suivre. C’est un petit plus côté performance, d’ailleurs. Sémantiquement, ça reste un micro-détail mais ça pourrait impacter du code velu que vous auriez :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Sloppy mode: `this` auto-boxing

function f() { return typeof this; }
console.log(f.call(42));   // => 'object'
console.log(f.call('yo')); // => 'object'
console.log(f.call(true)); // => 'object'

(function() {
  'use strict';

  function f() { return typeof this; }

  console.log(f.call(42));   // => 'number'
  console.log(f.call('yo')); // => 'string'
  console.log(f.call(true)); // => 'boolean'
})();

(Il suffit que la fonction appelée soit stricte : si f démarrait par un 'use strict';, des appels en mode négligé n’auto-boxeraient pas this non plus).

Duplication d’identifiants locaux ou de propriétés

Autre type d’erreur bien lourde à détecter : l’écrasement par erreur d’un identifiant, par exemple une variable locale (y compris un argument) ou une propriété d’objet.

Des fois, c’est assez facile à repérer :

1
2
3
4
5
6
7
8
// Sloppy mode

function register(name, age, sex, name) {
  return "Registered " + name + ', ' + sex + ' at age ' + age;
}

register('Élodie', 34, 'female', 'invite');
// => 'Registered invite, female at age 34'

Des fois c’est plus dur, parce qu’on a des objets longs, genre des contrôleurs ou modèles dans un framework MVC, et on ne voit même pas les deux propriétés conflictuelles sur le même écran. D’autant que ça vient en général d’un copier/coller de bonne foi entre deux fichiers :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Sloppy mode

var controller = {
  initialize: function initHome(title, entries) {
    this.title = title;
    this.entries = entries;
  },

  getRenderData: function getHomeRenderData() {
    return { title: this.title, entries: this.entries };
  },

  // … Plein d'autres méthodes ici …

  initialize: function initMain(subViews) {
    this.subViews = subViews;
  }
};

controller.initialize('Cool Product', ['yes', 'no', 'maybe']);

controller.getRenderData(); // => { title: undefined, entries: undefined }

Mais en mode strict, tous ces cas de figure lèvent immédiatement une erreur :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function() {
  'use strict';

  function register(name, age, sex, name) {
    return "Registered " + name + ', ' + sex + ' at age ' + age;
  }
})();

// => SyntaxError: Strict mode function may not have duplicate parameter names

(function() {
  'use strict';

  var controller = {
    initialize: function initHome(title, entries) {
      this.title = title;
      this.entries = entries;
    },

    getRenderData: function getHomeRenderData() {
      return { title: this.title, entries: this.entries };
    },

    // … Plein d'autres méthodes ici …

    initialize: function initMain(subViews) {
      this.subViews = subViews;
    }
  };
})();

// => SyntaxError: Duplicate data property in object literal not allowed in
//    strict mode

Et bim ! C’est déjà ça de pris.

eval qui pourrit la portée englobante

Il était possible pour du code passé à eval de « fuir » dans sa portée englobante, en y introduisant de nouveaux identifiants, ce qui présente des risques tant en termes de sécurité qu’en termes d’optimisation des lookups d’identifiants, et donc du code. Par exemple :

1
2
3
4
5
// Sloppy mode

var x = 37;
eval('var x = 42');
x; // => 42

Super, Michel… Mais en mode strict, eval c’est un peu comme Las Vegas : ce qui s’y passe y reste…

1
2
3
4
5
6
7
(function() {
  'use strict';

  var x = 37;
  console.log(eval('var x = 42; x')); // => 42
  console.log(x); // => 37
})();

Remarquez d’ailleurs que si eval hérite automatiquement le caractère strict de sa portée d’appel, il est aussi possible de faire un eval strict dans un contexte négligé : c’est un des cas « script entier » évoqués en début d’article.

1
2
3
4
5
// Sloppy mode, mais eval strict

var x = 37;
console.log(eval('"use strict"; var x = 42; x')); // => 42
console.log(x); // => 37

Ah, et puis tant qu’à faire, il n’est plus possible d’utiliser eval comme identifiant (argument, propriété, variable locale…). Mais tu ne faisais pas une chose pareille, n’est-ce pas ?

Pas de mauvaises pratiques

L’autre volet du mode strict, c’est l’interdiction de pratiques généralement considérées comme problématiques.

Recours à with

S’il existe quelques rares cas dans lesquels l’emploi de with facilite la vie, ça n’en reste pas moins un anti-pattern dans la mesure où ça complexifie le plus souvent la compréhension du code (en introduisant des tas d’identifiants « magiques », dont l’origine n’est pas évidente en analysant la portée lexicale), mais surtout parce que ça casse un gros paquet d’optimisations potentielles de lookup par la runtime, ce qui tue les performances du code concerné.

Le mode strict va refuser le mot-clé concerné :

1
2
3
4
5
6
7
8
9
(function(obj) {
  'use strict';

  with (obj) {
    console.log(name, applicationCache);
  }
})(window);

// => SyntaxError: Strict mode code may not include a with statement

Bon, en même temps, tu n’allais pas recoder un moteur de templating, hein ? Y’en a suffisamment comme ça (et puis Jade et Dust défoncent tout, chacun dans son style).

Triturage de .caller / .callee

Des petits malins s’amusent parfois à jouer avec la pile d’appels active au moyen de fx.caller, qui est d’ailleurs une extension non standard d’ECMAScript. Ça pose des soucis de sécurité. Cette propriété est interdite en mode strict : tenter de l’utiliser lève une erreur.

1
2
3
4
5
6
7
8
(function demo() {
  'use strict';

  console.log(demo.caller);
})();

// => TypeError: 'caller', 'callee', and 'arguments' properties may not be
//    accessed on strict mode functions or the arguments objects for calls to them

Même principe pour arguments.callee, qui donne une référence sur l’invocation en cours : il suffit de nommer la fonction, et si ce nommage gêne, d’en faire une expression de fonction pour que le nom soit strictement interne (comme pour une IIFE, par exemple) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function(n) {
  'use strict';

  console.log('Yo', n);
  if (n > 1) {
    arguments.callee(n - 1);
  }
})(5);

// => TypeError: 'caller', 'callee', and 'arguments' properties may not be
//    accessed on strict mode functions or the arguments objects for calls to
//    them

// Solution :

(function recurse(n) {
  'use strict';

  console.log('Yo', n);
  if (n > 1) {
    recurse(n - 1);
  }
})(5);

typeof recurse // => 'undefined'

Bidirectionnalité arguments / paramètres

Traditionnellement, la variable locale automatique arguments offre une liaison dynamique aux paramètres de la fonction : modifier l’un altère l’autre. Voici un exemple :

1
2
3
4
5
6
7
8
9
// Sloppy mode

(function(a, b) {
  arguments[0] = 'foo';
  console.log(a);            // => 'foo'

  b = 'bar';
  console.log(arguments[1]); // => 'bar'
})(42, 'hello');

En mode strict, les identifiants de paramètres et le contenu de arguments sont désolidarisés, ce qui permet de meilleures optimisations du code et évite les pièges de compréhension à la lecture :

1
2
3
4
5
6
7
8
9
(function(a, b) {
  'use strict';

  arguments[0] = 'foo';
  console.log(a);            // => 42

  b = 'bar';
  console.log(arguments[1]); // => 'hello'
})(42, 'hello');

Il devient par ailleurs impossible d’utiliser arguments comme identifiant pour autre chose, et la propriété non-standard fx.arguments lève une erreur au même titre que fx.caller ou arguments.callee.

Déclarations de fonctions hors de la racine de portée

Alors que ni ES3 ni ES5 n’ont jamais autorisé la déclaration de fonctions hors de la racine de portée (c’est-à-dire dans un bloc ou une structure de contrôle), la plupart des runtimes l’ont autorisé, mais avec des sémantiques divergentes, non garanties.

Pour rappel, une déclaration de fonction exploite le mot-clé function là où une instruction (par exemple une boucle for) est possible, contrairement à une expression de fonction. Voici quelques exemples :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Function Declaration
function foo() {}

// (Named) Function Expression
var foo = function foo() {};

// Anonymous Function Expression
var foo = function() {};

// Immediately-Invoked Function Expression
(function() {})();

// Function Expression as property
var obj = {
  foo: function foo() {}
};

S’il peut tout-à-fait arriver qu’on ait besoin d’une expression de fonction dans une structure de contrôle (par exemple pour capturer l’indice courant de la boucle dans une portée dédiée), les déclarations de fonctions, elles, sont censées être hoisted, donc interprétées au niveau racine de la portée avant toute interprétation du reste de la portée.

Histoire d’éviter les emmerdes, le mode strict interdit théoriquement les déclarations de fonctions hors de la racine de portée (ce n’est le cas ni dans Chrome 41 ni dans IE11, même si le TC39 le recommande depuis près de 5 ans) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function() {
  'use strict';

  if (true) {
    function foo() {}
  }

  // Ou encore :

  for (var index = 0; index < 5; ++index) {
    function bar(i) {}
    bar(index);
  }
})();

// Node, Firefox :
// => SyntaxError: in strict mode code, functions may be declared only at top
//    level or immediately within another function

// Safari :
// => SyntaxError: Strict mode does not allow function declarations in a
//    lexically nested statement.

Préparer le terrain pour de futures versions

ES5 réserve dans tous les cas les mots-clés connus des versions à venir, en particulier ES6 : class, enum, export, extends, import et super. Mais le mode strict réserve des identifiants qui pourraient constituer des mots-clés dans des versions ultérieures du langage. On se voit ainsi interdire implements, interface, let, package, private, protected, public, static et yield.

De fait, let et yield sont dans ES6 ; les autres ne sont pas encore là, mais sait-on jamais…

Pourquoi ES5, ES6 ou ES7 n’est pas strict par défaut ?

Parce qu’on ne peut pas casser le web.

En tant que développeur front-end, vous n’avez pas le choix du navigateur qui exécute votre script, donc pas le choix de votre runtime : si une runtime ES6 se mettait, par défaut, à interpréter votre code en mode strict, et que celui-ci n’est pas prévu pour, du code qui marchait jusqu’ici va casser sans que vous y ayez touché. À l’échelle du web, forcer ce type de changement d’interprétation n’est juste pas acceptable.

Il vous appartient donc de marquer vous-mêmes vos scripts comme conformes au mode strict, pour bénéficier des vérifications et optimisations associées.

Mais quand même…

Mon collègue et néanmoins ami™ Jonathan Blanchet me rappelle un truc dans l’oreillette

ES6 introduit deux nouveaux contextes lexicaux : les classes et les modules. Les deux sont automatiquement stricts, vu que ces codes ne risquaient pas de marcher avant ES6 :-)

Ceinture et bretelles : JSHint + mode strict

Il reste indispensable d’avoir un linter en place sur votre code, idéalement en arrière-plan, intégré à votre éditeur / EDI. Les trois principaux sont JSLint (« le vieux con »), JSHint (« le super pote ») et ESLint (« le p’tit nouveau qu’a des super gadgets »).

Prenez le temps de découvrir comment intégrer celui de votre choix à votre éditeur / EDI chéri (si ce n’est pas fait par défaut), et collez-vous une configuration par défaut bien fichue (la mienne pour JSHint) à la racine de votre compte ou de votre dossier de travail. Et surtout, calez-le à la volée (genre après 2 secondes d’inactivité), pas juste à l’ouverture ou à la sauvegarde. C’est extrêmement précieux comme assistance au code.

Ajoutez le même linting comme condition sine qua non à vos tests, votre intégration continue, et même pourquoi pas vos hooks de pre-commit.

Mais par-dessus le marché, migrez vos scripts en mode strict. Au début, ça va piquer, mais c’est tout ça de qualité en plus et de débogage en moins pour la suite.

Envie d’en savoir plus ?

L’article du Mozilla Developer Network sur le mode strict décrit tout ça à sa sauce, ça peut servir de complément d’explication. Leur article connexe, Passer au mode strict, peut vous aider à mettre votre code en conformité.

Par ailleurs, on fait des formations JS magnifiques, qui font briller les yeux et frétiller les claviers :

  • JS Total, pour apprendre tout ce qu’il faut pour faire du dev web front moderne, efficace, rapide et haut de gamme.
  • Node.js, pour découvrir, apprivoiser puis maîtriser le nouveau chouchou de la couche serveur, qui envoie du gros pâté !

Commentaires

Ils nous font confiance : Kelkoo, MisterGoodDeal, PriceMinister, Blablacar / Comuto, Sarenza, Voyages-SNCF, LeMonde.fr, Fnac DIRECT, 20minutes, Orange, l’OCDE, Cisco, Alcatel-Lucent, Dassault Systèmes, EADS, Atos, Lagardère Interactive, Lesieur, L’Occitane en Provence, Météo France, 4D, Securitas, Digitas, Vivaki, Fullsix, Ekino, TBWA \ Paris, Valtech, Is Cool Entertainment, Open Wide…