. Côté Node, deux options : utiliser l'extension .mjs, ou ajouter \"type\": \"module\" dans le package.json."}},{"@type":"Question","name":"Quelle différence entre export par défaut et exports nommés ?","acceptedAnswer":{"@type":"Answer","text":"Un module peut avoir autant d'exports nommés qu'on veut (export function foo() {}), mais un seul export par défaut (export default ...). Les exports nommés s'importent obligatoirement avec le même nom, entre accolades : import { foo } from './x.js'. L'export par défaut, lui, peut être importé sous le nom qu'on veut : import peuImporte from './x.js'."}},{"@type":"Question","name":"C'est quoi l'import dynamique en JavaScript ?","acceptedAnswer":{"@type":"Answer","text":"import() utilisé comme une fonction renvoie une promesse qui se résout avec les exports du module. Contrairement au import statique, il s'exécute au moment de l'appel — on peut donc charger du code à la demande ou sous condition. C'est ce qui permet de faire du code-splitting et du lazy loading."}},{"@type":"Question","name":"Faut-il mettre l'extension du fichier dans les chemins d'import ?","acceptedAnswer":{"@type":"Answer","text":"Avec les modules ES natifs (navigateur et loader ESM de Node), oui, c'est obligatoire : il faut écrire ./utils.js, pas ./utils. Les bundlers comme Vite ou webpack sont plus souples et résolvent les chemins sans extension, mais s'appuyer là-dessus rend le code non portable."}}]}
Menu
Français

Modules ES en JavaScript : import, export et import dynamique

Comprendre les modules ES en JavaScript : exports nommés et par défaut, syntaxe d'import, import() dynamique et ce qui distingue un module d'un script classique.

Un module, c'est un fichier avec sa propre portée

Avant l'arrivée des modules ES, chaque balise <script> déversait ses variables dans l'espace global, et l'ordre de chargement décidait qui voyait quoi. Les modules ES JavaScript règlent le problème : chaque fichier a sa propre portée. Rien ne fuit vers l'extérieur sauf si tu fais un export explicite, et rien n'entre sauf si tu fais un import explicite.

Deux fichiers, l'un qui exporte, l'autre qui importe :

index.js
Output
Click Run to see the output here.

add et multiply vivent dans math.js. Ils ne deviennent visibles dans main.js qu'à cause de l'import. Tout le reste de math.js — fonctions utilitaires, constantes, peu importe — reste inaccessible depuis l'extérieur.

Deux règles en découlent, et autant les intégrer tout de suite :

  • Les modules tournent automatiquement en mode strict. Pas besoin de 'use strict'.
  • Au plus haut niveau, this vaut undefined, pas l'objet global.

Exports nommés : on exporte au fil de l'eau

C'est la forme la plus courante. Colle export devant n'importe quelle function, class, const ou let et le voilà qui rejoint la surface publique du module :

index.js
Output
Click Run to see the output here.

Les noms entre accolades doivent correspondre exactement aux noms exportés — import { circlearea } échouerait. Si un nom entre en conflit avec quelque chose qui existe déjà chez toi, renomme-le à l'import avec as :

index.js
Output
Click Run to see the output here.

Tu peux aussi regrouper tes exports en bas de fichier plutôt que de les déclarer au fil de l'eau — certains préfèrent cette approche, qui met bien en évidence l'« API publique » du module :

index.js
Output
Click Run to see the output here.

Les deux styles donnent le même résultat. Choisissez-en un et gardez-le sur l'ensemble du projet.

Export par défaut : un seul par module

Un module peut aussi avoir un unique export par défaut. L'export default est idéal pour les fichiers qui ont vraiment une chose principale à exposer — un composant, une classe, un objet de configuration :

index.js
Output
Click Run to see the output here.

Trois points à retenir :

  • Pas d'accolades autour de log côté import.
  • Le nom à l'import, c'est toi qui le choisis. import shout from './logger.js' fonctionnerait exactement pareil.
  • Un seul export default par fichier. Si tu en ajoutes un deuxième, le fichier refuse de se parser.

Les exports nommés et l'export default peuvent cohabiter dans un même fichier :

index.js
Output
Click Run to see the output here.

D'abord le default, puis les exports nommés entre accolades. L'ordre est imposé.

Lequel choisir ? Les exports nommés sont plus faciles à refactorer : renommer un symbole dans toute une base de code revient à un simple rechercher-remplacer, puisque chaque import utilise le même nom. Les export default offrent plus de souplesse, mais chaque appelant peut choisir un nom différent, ce qui rend le grep beaucoup moins efficace. La plupart des guides de style modernes privilégient les exports nommés et réservent export default aux modules qui ont vraiment une seule responsabilité.

Importer tout un module, réexporter, effets de bord

Voici quelques autres formes d'import que vous allez croiser.

Pour récupérer tous les exports nommés dans un seul objet espace de noms :

index.js
Output
Click Run to see the output here.

Ré-exporter depuis un autre module sans l'importer dans la portée courante :

index.js
Output
Click Run to see the output here.

Ce schéma est exactement ce que font les points d'entrée de librairies pour regrouper des morceaux éparpillés dans plusieurs fichiers internes en une seule surface publique.

Et pour finir, il existe aussi l'import sans aucune liaison — pratique pour les modules dont le seul rôle est de produire des effets de bord (polyfills, CSS-in-JS, enregistrement de handlers) :

index.js
Output
Click Run to see the output here.

Le fichier s'exécute une seule fois ; rien n'est importé par son nom.

Les imports sont statiques et vivants

Deux caractéristiques d'import qui surprennent parfois.

Statique. Les déclarations import sont résolues avant que la moindre ligne de votre code ne tourne. Impossible donc d'en glisser une dans un if, une fonction ou un try. Le chemin doit obligatoirement être une chaîne littérale — pas une variable. C'est précisément ce qui permet aux outils d'analyser les imports sans exécuter le code : bundlers, vérificateurs de types et tree-shakers reposent tous là-dessus.

// Non autorisé — SyntaxError.
if (userWantsFancy) {
  import { fancy } from './fancy.js';
}

Si tu as besoin d'un chargement conditionnel, utilise import() (on y vient juste après).

Vivante. Une liaison importée est une référence en lecture seule vers l'export, pas une copie figée. Si le module qui exporte réassigne la valeur, ceux qui l'importent voient la nouvelle valeur :

index.js
Output
Click Run to see the output here.

Côté consommateur, tu ne peux pas non plus réaffecter un import : faire count = 5 dans main.js déclencherait une erreur. Les imports sont des vues en lecture seule.

Charger à la demande avec import() dynamique

Quand tu dois décider à l'exécution s'il faut charger un module — fonctionnalités lourdes, code splitting par route, polyfills conditionnels — utilise import() comme une fonction. Elle renvoie une promesse qui se résout avec les exports du module :

index.js
Output
Click Run to see the output here.

Comme il s'agit d'un simple appel de fonction, tu peux :

  • L'attendre avec await dans une fonction async.
  • Lui passer une variable comme chemin.
  • L'utiliser dans un if ou un try/catch.

Le déstructurage de l'objet résolu fonctionne exactement comme avec un import statique :

index.js
Output
Click Run to see the output here.

default est la clé utilisée pour l'export par défaut quand tu fais une déstructuration. Libre à toi de la renommer comme tu veux.

Les cas d'usage concrets ? Le code-splitting (ne charger la bibliothèque de graphiques que lorsque l'utilisateur clique sur « afficher le graphique »), les polyfills conditionnels selon la détection de fonctionnalités, ou encore les plugins découverts à l'exécution.

Exécuter des modules ES : navigateurs et Node.js

La syntaxe reste identique partout ; ce qui change, c'est la façon dont le runtime localise et charge le fichier.

Côté navigateur, il suffit de déclarer le script d'entrée comme un module :

<script type="module" src="./main.js"></script>

Avec type="module", le navigateur prend en compte les import/export, exécute le code en mode strict et diffère l'exécution jusqu'à ce que le HTML soit parsé. Les chemins doivent être relatifs (./, ../) ou des URLs absolues — les spécificateurs nus comme import 'lodash' ne fonctionnent pas sans import map ni bundler.

Côté Node, il existe deux façons d'activer les modules ES :

  • Nommer le fichier avec l'extension .mjs, ou
  • Définir "type": "module" dans le package.json le plus proche, ce qui transforme chaque fichier .js en module.

Node impose aussi des chemins complets avec l'extension : import './utils.js', et non import './utils'.

// package.json
{
  "type": "module",
  "main": "./index.js"
}

Les deux environnements exigent des extensions explicites en ESM natif. Les bundlers (Vite, webpack, esbuild) résolvent les chemins sans extension pendant le développement — pratique, mais s'en remettre à eux, c'est accepter que votre code source ne tournera pas sans l'étape de build.

Pièges classiques à éviter

Quelques erreurs qui reviennent souvent :

  • Oublier type="module" dans le navigateur. Sans ça, la balise <script> est exécutée comme un script classique, et import déclenche une erreur de syntaxe.
  • Omettre les extensions de fichier dans Node. import './utils' échoue ; import './utils.js' fonctionne. Les bundlers masquent ce détail, les runtimes natifs non.
  • Compter sur __dirname ou require dans un module ES. Ces éléments sont réservés à CommonJS. En ESM, utilisez import.meta.url et convertissez-le si vous avez besoin d'un chemin.
  • Imports circulaires qui accèdent à des valeurs non initialisées. Deux modules qui s'importent mutuellement, c'est autorisé, mais lire un export pas encore assigné vous renverra undefined. Organisez votre code pour que le cycle ne soit pas traversé à l'initialisation, ou cassez-le.
  • Vouloir faire un import conditionnel. L'instruction import statique ne le permet pas. Passez par l'import() dynamique pour tout ce qui dépend du runtime.

La suite : CommonJS vs ESM

Les modules ES sont devenus le standard, mais une bonne partie du code Node en circulation utilise encore CommonJS — avec require, module.exports, et une logique différente concernant le moment où le code s'exécute. Connaître les deux, et savoir comment ils s'interfacent, c'est le sujet de la page suivante.

Questions fréquentes

Comment utiliser les modules ES en JavaScript ?

On expose des valeurs depuis un fichier avec export ou export default, puis on les récupère ailleurs avec import. Côté navigateur, il faut charger le point d'entrée avec <script type="module" src="main.js"></script>. Côté Node, deux options : utiliser l'extension .mjs, ou ajouter "type": "module" dans le package.json.

Quelle différence entre export par défaut et exports nommés ?

Un module peut avoir autant d'exports nommés qu'on veut (export function foo() {}), mais un seul export par défaut (export default ...). Les exports nommés s'importent obligatoirement avec le même nom, entre accolades : import { foo } from './x.js'. L'export par défaut, lui, peut être importé sous le nom qu'on veut : import peuImporte from './x.js'.

C'est quoi l'import dynamique en JavaScript ?

import() utilisé comme une fonction renvoie une promesse qui se résout avec les exports du module. Contrairement au import statique, il s'exécute au moment de l'appel — on peut donc charger du code à la demande ou sous condition. C'est ce qui permet de faire du code-splitting et du lazy loading.

Faut-il mettre l'extension du fichier dans les chemins d'import ?

Avec les modules ES natifs (navigateur et loader ESM de Node), oui, c'est obligatoire : il faut écrire ./utils.js, pas ./utils. Les bundlers comme Vite ou webpack sont plus souples et résolvent les chemins sans extension, mais s'appuyer là-dessus rend le code non portable.

Apprendre à coder avec Coddy

COMMENCER