Pertama kali membangun program kasir, Budi menaruh semuanya dalam satu berkas kasir.js: daftar produk, rumus diskon, perhitungan pajak, sampai penyusunan teks struk. Selama isi program masih beberapa ratus baris, satu berkas terasa praktis — cukup buka satu tab editor. Persoalan mulai terasa ketika fitur bertambah: fungsi hitungTotal untuk diskon ternyata benturan nama dengan fungsi perhitungan layanan yang ditulis belakangan, dan Budi menghabiskan waktu lama hanya untuk mencari letak baris yang perlu diubah.
JavaScript modern menjawab persoalan itu dengan module: berkas JavaScript yang punya scope sendiri dan dapat menawarkan nilainya ke berkas lain lewat export, serta mengambil nilai dari berkas lain lewat import. Chapter ini memetakan alasan pemecahan program, perbedaan module dari script klasik, dan dua tempat module dijalankan — browser dan Node.js. Seluruh contoh section ini kita uji dengan Node 24 di terminal, tetapi sintaks yang sama berjalan di browser modern tanpa perubahan.
Masalah Satu Berkas Panjang #
Menyatukan seluruh kode dalam satu berkas menimbulkan beberapa persoalan yang makin terasa seiring program membesar:
- Benturan nama. Semua deklarasi berada dalam satu ruang lingkup. Dua fungsi bernama sama saling menimpa tanpa peringatan, dan pemanggilan yang salah sasaran sulit dilacak.
- Urutan berkas yang rapuh. Tanpa module, satu halaman memuat beberapa tag
<script>dan kita wajib mengingat urutannya; menukar posisi dua tag dapat membuat program gagal karena variabel yang dibutuhkan belum tersedia. - Kebocoran ke lingkup global. Deklarasi
vardanfunctionpada script klasik menempel pada objek global, sehingga skrip pihak ketiga yang dimuat belakangan dapat membaca atau menimpanya. - Sulit dinavigasi dan dirawat. Satu berkas seribu baris memaksa kita menggulir jauh; perubahan satu fitur berisiko menyentuh fitur lain karena batas antar bagian hanya berupa komentar.
Module menjawab keempatnya: setiap berkas memiliki scope sendiri, ketergantungan antar berkas dinyatakan eksplisit lewat import, dan setiap fitur menempati berkas dengan tanggung jawab yang jelas.
ES Modules: Standar Bawaan Bahasa #
Sebelum 2015, JavaScript tidak memiliki sistem module resmi. Komunitas mengisi kekosongan itu dengan CommonJS — sistem require() yang populer di Node.js — serta beberapa format lain untuk browser. ECMAScript 2015 kemudian menambahkan export dan import sebagai bagian resmi bahasa; format inilah yang disebut ES Modules. Kini ES Modules berjalan native di browser modern maupun Node.js tanpa alat tambahan, dan format itulah yang dipakai di seluruh course ini.
Module juga berperilaku berbeda dari script klasik pada beberapa hal penting:
| Perilaku | Script klasik | Module |
|---|---|---|
| Scope teratas | menempel ke objek global | scope sendiri per berkas |
| Mode strict | perlu dinyatakan manual | otomatis aktif |
| Eksekusi di browser | mengikuti urutan tag, menghambat penguraian HTML | ditunda sampai HTML selesai diuraikan |
| Jumlah evaluasi | sekali per tag yang memuat | sekali per module, lalu disimpan di cache |
Dua baris terakhir tabel ini akan kita buktikan di chapter Module Scope. Untuk saat ini, cukup catat bahwa module lebih tertib daripada script klasik pada setiap aspek di atas.
Mode Module di Browser #
Browser mengenali module lewat atribut type="module" pada tag <script>:
<!-- halaman.html — memuat module dari browser -->
<script type="module" src="./utama.mjs"></script>Dengan atribut tersebut, browser memperlakukan utama.mjs sebagai module: berkas itu boleh memuat import dan export, eksekusinya ditunda sampai dokumen selesai diuraikan — perilaku yang setara dengan defer — dan jalur pada src ditulis sebagai jalur relatif yang lengkap dengan ekstensinya, yaitu ./utama.mjs, bukan utama. Server web juga perlu menyajikan berkas module dengan tipe MIME JavaScript yang benar; hal itu beres otomatis bila berkas di-host melalui server web biasa.
Mode Module di Node.js #
Node.js menentukan mode sebuah berkas dari ekstensinya. Berkas .mjs selalu diperlakukan sebagai ES Modules, sehingga kita dapat langsung menjalankan node utama.mjs tanpa berkas konfigurasi apa pun. Alternatifnya, sebuah proyek Node dapat menyatakan "type": "module" pada package.json agar semua berkas .js diperlakukan sebagai module — cara itu akan tersinggung lagi di section Tooling.
Satu aturan Node yang perlu dibiasakan sejak awal: specifier, yaitu teks jalur di belakang kata from, wajib berupa jalur relatif yang diawali ./ atau ../ dan mencantumkan ekstensi. Penulisan from './tarif.mjs' sah; penulisan from 'tarif' akan ditolak karena Node menganggapnya nama package, bukan berkas proyek.
Contoh Pertama: Program Dua Berkas #
Kita buktikan mekanismenya dengan kasir kafe milik Budi yang baru dipecah menjadi dua berkas. Berkas tarif.mjs menawarkan aturan harga dasar: konstanta tarif layanan dan fungsi penghitung total belanja:
// tarif.mjs — aturan harga dasar untuk kasir kafe
export const TARIF_LAYANAN = 0.05;
export function hitungTotal(harga, jumlah) {
return harga * jumlah;
}Berkas utama.mjs menjadi titik masuk program: berkas ini mengambil kedua nilai dari tarif.mjs, lalu menyusun perhitungan biaya layanan:
// utama.mjs — titik masuk: menggabungkan aturan dari tarif.mjs
import { TARIF_LAYANAN, hitungTotal } from './tarif.mjs';
const total = hitungTotal(18000, 2);
const biayaLayanan = Math.round(total * TARIF_LAYANAN);
console.log(total);
console.log(biayaLayanan);
console.log(total + biayaLayanan);Output yang Dihasilkan:
36000
1800
37800Mari kita telaah kedua berkas tersebut:
- Baris 2
tarif.mjs(export const TARIF_LAYANAN = 0.05;): kataexportdi depan deklarasi menawarkan konstanta ini ke luar; berkas lain dapat mengimpornya dengan nama yang sama. - Baris 4–6
tarif.mjs(export function hitungTotal(...)): fungsi juga diexport dengan cara yang sama. Badan fungsinya sengaja kecil — satu module boleh sederhana, tanggung jawabnya jelas. - Baris 2
utama.mjs(import { TARIF_LAYANAN, hitungTotal } ...): kurung kurawal memuat daftar nama yang diambil daritarif.mjs. Jalur./tarif.mjsrelatif terhadap lokasiutama.mjsdan mencantumkan ekstensi, sesuai aturan Node. - Baris 4–5
utama.mjs(const total ... const biayaLayanan ...): setelah nilai diimpor, keduanya dipakai seperti variabel biasa.Math.roundmenjaga hasil perkalian desimal tetap bilangan bulat. - Baris 7–9
utama.mjs(console.log(...)): tiga keluaran tersebut adalah bukti nyata bahwaexportdanimportbekerja — nilai36000berasal dari fungsi di berkas lain, dan0.05dari konstanta di berkas yang sama.
Program dua berkas ini sudah menunjukkan seluruh siklus dasar: satu berkas menawarkan nilai dengan export, berkas lain mengambilnya dengan import, dan Node mengeksekusi utama.mjs sebagai titik masuk. Chapter berikutnya memperdalam sisi penawaran: bagaimana satu module menawarkan banyak nilai sekaligus dengan named exports.