Idealnya, ketergantungan antar module mengalir satu arah: utama.mjs mengimpor pajak.mjs, dan pajak.mjs tidak mengimpor balik. Kenyataannya, pemecahan program tidak selalu secermat itu. Saat fitur berkembang, dua module bisa saling membutuhkan — pajak.mjs membutuhkan aturan diskon, sementara diskon.mjs membutuhkan perhitungan pajak — dan JavaScript justru mengizinkan situasi ini dengan aturan tertentu.
Chapter ini menunjukkan circular dependency yang benar-benar berjalan, membedah mengapa ia tidak melempar galat, lalu membahas cara memecahnya ketika siklus itu tidak diinginkan. Seluruh keluaran diuji nyata dengan node. Contohnya sengaja dibuat kecil — hanya dua module — supaya siklusnya mudah dilacak mata dan urutan evaluasinya bisa disusuri baris demi baris.
Dua Module yang Saling Membutuhkan #
Kasir milik Budi punya dua aturan yang saling terkait. Pajak dihitung dari nilai setelah diskon, sehingga pajak.mjs membutuhkan potongan dari diskon.mjs. Sebaliknya, laporan diskon.mjs perlu tahu pajak atas sebuah transaksi, sehingga ia mengimpor balik dari pajak.mjs. Berkas pertama:
// pajak.mjs — perhitungan pajak; membutuhkan potongan dari diskon.mjs
import { potongan } from './diskon.mjs';
export const TARIF = 0.11;
export function dasarKenaPajak(total) {
return total - potongan(total);
}
export function totalBayar(total) {
return Math.round(dasarKenaPajak(total) * (1 + TARIF));
}Berkas kedua mengimpor balik dari pajak.mjs — di sinilah siklusnya terbentuk:
// diskon.mjs — aturan potongan; membutuhkan dasar kena pajak dari pajak.mjs
import { dasarKenaPajak, TARIF } from './pajak.mjs';
export let jumlahPotongan = 0;
export function potongan(total) {
if (total >= 100000) {
jumlahPotongan += 1;
return 10000;
}
return 0;
}
export function pajakTransaksi(total) {
return Math.round(dasarKenaPajak(total) * TARIF);
}Berkas pengimpor tidak perlu tahu adanya siklus; ia cukup memakai kedua module:
// utama.mjs — memakai dua module yang saling membutuhkan
import { totalBayar } from './pajak.mjs';
import { pajakTransaksi, jumlahPotongan } from './diskon.mjs';
const total = 150000;
console.log(totalBayar(total));
console.log(pajakTransaksi(total));
console.log(jumlahPotongan);Output yang Dihasilkan:
155400
15400
2Perhitungannya lurus: dari total 150000, potongan 10000 menghasilkan dasar kena pajak 140000, sehingga totalBayar menghasilkan 155400 dan pajakTransaksi mencatat pajak 15400. Angka terakhir 2 adalah hitungan berapa kali potongan benar-benar dipanggil — sekali lewat totalBayar, sekali lagi lewat pajakTransaksi. Kedua module yang saling mengimpor bekerja tanpa satu pun galat.
Urutan Evaluasi di Balik Siklus #
Mengapa program ini tidak berbenturan dengan dirinya sendiri? Jawabannya ada pada dua fase yang berbeda:
- Fase penguraian (linking). Sebelum eksekusi, Node memetakan seluruh graf ketergantungan:
utama.mjs→pajak.mjs→diskon.mjs→ kembali kepajak.mjs. Saat menemukanpajak.mjsuntuk kedua kalinya, Node tidak mengevaluasinya ulang — ia mencatat siklusnya dan melanjutkan. Pada fase ini semua nama export sudah terdaftar, walaupun nilainya belum tentu terisi. - Fase evaluasi. Node mengevaluasi module secara depth-first: karena
pajak.mjsmengimpordiskon.mjs, urutan eksekusi badan module menjadidiskon.mjslebih dulu, lalupajak.mjs, baruutama.mjs. Kedua berkas hanya berisi deklarasiconstdan deklarasi fungsi di tingkat atas — tidak ada pemanggilan fungsi yang berjalan saat evaluasi — sehingga tidak ada yang perlu membaca nilai yang belum terisi.
Deklarasi fungsi adalah kunci amannya: fungsi di-hoisting penuh sejak awal evaluasi, sehingga potongan di diskon.mjs sudah siap dipanggil meskipun badan diskon.mjs sendiri dievaluasi lebih dulu. Seluruh pemanggilan lintas module di contoh ini baru terjadi lewat utama.mjs — jauh setelah kedua module selesai dievaluasi.
Kita susuri urutannya berdasarkan berkas yang sudah ditulis. Node memulai dari utama.mjs, menemukan impor ./pajak.mjs, dan langsung beralih mengevaluasinya. pajak.mjs menemukan impor ./diskon.mjs, sehingga badan diskon.mjs dievaluasi lebih dulu — meskipun utama.mjs menulis impor pajak.mjs di baris lebih awal. diskon.mjs mengimpor pajak.mjs yang tengah dievaluasi; Node tidak mengulanginya, cukup mencatat siklusnya. Badan diskon.mjs selesai, badan pajak.mjs menyusul, dan terakhir badan utama.mjs berjalan hingga tiga keluaran tercetak.
Batasnya: Partial Bindings #
Situasi di atas aman karena urutan aksesnya benar. Tapi fase linking memang membuat semua nama terdaftar lebih dulu, dan di situlah istilah partial binding berlaku: ketika sebuah module tengah dievaluasi di tengah siklus, module lain dapat melihat nama-namanya yang belum terisi. Bila contoh di atas diubah — misalnya diskon.mjs membaca TARIF langsung di tingkat atas dengan const salin = TARIF; — eksekusinya berhenti karena saat itu pajak.mjs belum sempat mengisi TARIF:
ReferenceError: Cannot access 'TARIF' before initializationPola aman yang bisa kita pegang: di dalam siklus, pastikan nilai tingkat atas dari module lain hanya diakses di dalam badan fungsi — bukan saat evaluasi module berjalan. Contoh utama chapter ini mengikuti pola itu sepenuhnya, sehingga tidak ada satu pun ReferenceError pada jalannya program. Bila nilai memang harus tersedia saat evaluasi, beri akses lewat fungsi pengakses: pengimpor memanggil fungsi tersebut nanti, ketika nilai sudah pasti terisi.
Live Binding sebagai Bukti #
Keluaran 2 di atas juga membuktikan sifat live binding yang disinggung di chapter Named Exports. Variabel jumlahPotongan diexport sebagai let dan diubah dari dalam diskon.mjs setiap kali potongan diberikan. utama.mjs tidak pernah mengubahnya, tetapi tetap melihat nilai terbaru 2 — impor bernama adalah tampilan langsung ke module asalnya, bukan salinan yang dibekukan saat impor. Jika utama.mjs membaca jumlahPotongan lagi setelah transaksi berikutnya, nilainya terus bertambah tanpa impor ulang apa pun.
Cara Memecah Ketergantungan Melingkar #
Siklus yang bekerja bukan berarti layak dipertahankan. Ketergantungan melingkar membuat dua module tidak bisa dipahami sendiri-sendiri, dan pengujian satu module mesti menyeret module pasangannya. Untuk mengenali siklus di proyek yang lebih besar, gambarkan graf ketergantungannya: satu kotak per berkas, satu panah per pernyataan impor — siklus adalah rangkaian panah yang kembali ke kotak asalnya. Alat linter umumnya menyediakan aturan pendeteksi siklus otomatis, topik yang tersambung di section Tooling dan Code Quality. Tiga jalan keluar yang lazim:
- Pindahkan aturan bersama ke module ketiga. Bagian yang dibutuhkan keduanya — di sini
potongan— diangkat ke module baru, misalnyaaturan-dasar.mjs, lalu kedua module mengimpornya. - Kirim nilai lewat parameter. Bila satu arah hanya membutuhkan satu angka, kirimkan angka itu sebagai argumen alih-alih mengimpor module pasangannya. Pada contoh kita,
pajakTransaksisebenarnya hanya membutuhkan angka dasar kena pajak — angka itu dapat dikirim pemanggil, dan siklusnya hilang tanpa perlu berkas baru. - Gabungkan keduanya. Bila kedua module memang satu kesatuan aturan harga, menyatukannya jujur daripada memaksakan batas berkas yang bocor.
Sebelum (siklus): Sesudah (satu arah):
pajak.mjs ⇄ diskon.mjs pajak.mjs → aturan-dasar.mjs ← diskon.mjsDengan siklus yang terkendali — atau yang sudah dipecah — fondasi module praktis lengkap. Yang tersisa adalah pertanyaan skala: ketika jumlah module bertambah banyak, bagaimana menyusunnya agar mudah ditemukan dan diimpor? Itu topik chapter penutup section ini.