Modules
Intro
// modules weren't part of JS language for a long time and people used third party libraries
// now they are present and part of standard since 2015
// a module is simply a .js file (i.e. a script)
/*
Features of modules:
- Can only be used in strict mode.
- Each module has its own top-level scope. So global vars of a module aren't accessible in other modules.
- A module code is evaluated only the first time when its imported.
- When a variable or object is imported, its reference is imported and a new copy isn't made. Exports are created once and shared between importers.
*/
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`)
}
// 📁 main.js
import {sayHi} from './sayHi.js'
alert(sayHi) // function
sayHi('John') // Hello, John!
// a module code is evaluated only the first time when its imported
// 📁 alert.js
alert("Module is evaluated!")
// 📁 1.js
import `./alert.js`
// 📁 2.js
import `./alert.js`
// 📁 3.js
import `./alert.js`
// prints "Module is evaluated!" only once, evaluated only on 1.js import
// when a variable or object is imported, its reference is imported and a new copy isn't made
// 📁 admin.js
export let admin = {
name: "John"
}
// 📁 1.js
import {admin} from './admin.js'
admin.name = "Pete"
// 📁 2.js
import {admin} from './admin.js'
alert(admin.name) // Pete
// Both 1.js and 2.js reference the same admin object, changes made in 1.js are visible in 2.js
/*
Build tools like Webpack do the following:
- build a single file with all modules (or fewer files)
- unreachable code removed
- unused exports removed ("tree-shaking")
- evelopment-specific statements like console and debugger removed
- modern, bleeding-edge JavaScript syntax may be transformed to older one with similar functionality using Babel
- the resulting file is minified (spaces removed, variables replaced with shorter names, etc)
So basically they overwrite every module configuration that we do in lieu of performance optimization.
*/
Export/Import
// we can export variables, functions, or classes
// just add a export keyword before normal declarations (named exports)
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
export const CURRENT_YEAR = 2025
export function sayHi(user) {
alert(`Hello, ${user}!`)
} // no ; at the end
// export separate from declarations; place this at the bottom of the module
export {months, CURRENT_YEAR, sayHi}
// named imports
import {sayHi, sayBye} from './say.js'
sayHi()
sayBye()
// import all
import * as say from './say.js'
say.sayHi()
say.sayBye()
// rename imports
import {sayHi as hi, sayBye as bye} from './say.js'
// rename exports
export {sayHi as hi, sayBye as bye}
// export default - used to specify default export of a module, hence only one such export per module
export default function sayHi(user) {
alert(`Hello, ${user}!`)
}
import sayHi from './say.js' // no curly braces
// technically, we may have both default and named exports in a single module (anti-pattern) (use "default" as name)
// 📁 user.js
export default class User {
constructor(name) {
this.name = name
}
}
export function sayHi(user) {
alert(`Hello, ${user}!`)
}
// 📁 main.js
import {default as User, sayHi} from './user.js'
new User('John')
// "default" name with import all
import * as user from './user.js'
let User = user.default // the default export
new User('John')
// re-export
export {sayHi} from './say.js' // re-export sayHi
// re-exporting the default export needs special considerations
Dynamic Import
// we can't dynamically generate any parameters of import
// to do so use import(), it is not a function, just a special syntax with parentheses
// we can use it anywhere in code and it returns a promise that resolves into a module object that contains all its exports
let {hi, bye} = await import('./say.js')
// or in case of a default export
let obj = await import('./say.js')