Módulos
¿Por qué usar módulos?
Añadir manualmente todos los archivos de JavaScript que vamos a utilizar en nuestro HTML (clases, funciones, constantes, librerías, etc.) puede llegar a ser problemático en un proyecto grande y complejo. Podría generar problemas de sobreescritura de variables o funciones globales, incluir mucho más código del necesario en la aplicación, "ensuciar" el HTML, etc.
La división de nuestra aplicación en módulos (cada archivo es un módulo), está soportada por JavaScript desde la versión ES2015, y permite que en cada módulo, o archivo, podamos decidir que variables, funciones, clases, etc. exportaremos para usarlas desde otros módulos (archivos). De esta manera solo tenemos que incluir un (o pocos) archivo principal en el HTML y desde ahí importaremos el resto de cosas que necesitemos. Las variables serán locales siempre a cada módulo , reduciendo el riesgo de solapamiento de código, y los efectos colaterales que pueden tener variables y funciones globales a toda la aplicación.
Para utilizar módulos, solamente tenemos que declarar el script importado en html como type="module". Lo único malo de esto es que debemos ejecutar nuestra aplicación bajo un servidor web para que funcione.
Al incluir este atributo, la característica defer viene implícita, por lo que ya no es necesario añadir dicho atributo.

Instalar Live Server
Visual Studio Code tiene una extensión muy útil, Live Server, que permite ejecutar un servidor web ligero con un click de ratón (y pararlo de la misma manera).

Abajo a la derecha, verás una sección donde aparece "Go live". Con un solo click, se ejecutará el servicio, y se abrirá un navegador en http://localhost:5500 donde podrás probar la aplicación.

Ahora ya podremos añadir los scripts con el atributo type="module" a nuestro documento HTML:
Funcionamiento de los módulos
Cada archivo es un módulo, y en principio todo es privado (variables, constantes, clases, funciones) a dicho módulo (no accesible desde otros módulos). Para que otros módulos puedan usar por ejemplo, funciones o clases del módulo actual, debemos exportarlos.
export default
La forma más simple sería un módulo que sólo exporta una cosa. Por ejemplo, un archivo donde sólo hay definida una clase y queremos utilizarla desde otros módulos. Usando la sintaxis export default exportamos el elemento en cuestión, que importamos desde otros módulos (podemos importarlo con cualquier nombre).
export múltiple
Si queremos exportar más de un elemento (o si queremos exportarlo con un nombre predefinido), no usaremos la palabra default, sino que lo que exportemos debe de tener un nombre. De esta manera podemos exportar varios elementos. Ahora al importar, debemos indicar qué elementos queremos importar desde el otro módulo entre llaves { }.
Otra manera de exportar elementos (equivalente a la anterior) es declarar al final del archivo lo que este exportará. Al igual que con el método anterior, no tenemos por qué importar todo lo que exporta un módulo desde otro, sólo lo que necesitamos.
Otras formas de importar
También podemos asignar un alias a cualquier cosa que importemos con la palabra reservada as:
Otra opción es importar todo lo que un módulo exporta dentro de un objeto. Esto implica que para acceder a cualquier elemento importado (clase, constante, función), debemos acceder a él como una propiedad del objeto creado. Para ello creamos un alias (nombre del objeto) e importamos con esta sintaxis:
Esta sintaxis previa para importar sería lo más cercano a la clásica sintaxis require de NodeJS también llamada CommonJS (CJS). La sintaxis moderna con export e import, también soportada por NodeJs en la actualidad, se conoce como ES Modules (ESM).
Por último, si queremos ejecutar un archivo JavaScript que no esté pensado como un módulo (una librería antigua por ejemplo), y que no exporta nada, simplemente importamos el archivo. En este caso, se ejecutará y procesará todo el contenido del archivo creando entre otras cosas las variables, funciones, etc. definidas en el mismo y siendo accesibles al módulo actual.