Menu

ES Modules JavaScript: import, export, dan Pemuatan Dinamis

Cara kerja ES modules di JavaScript: named export dan default export, sintaks import, import() dinamis, serta aturan yang membedakan module dari script biasa.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Module Adalah File dengan Scope Sendiri

Sebelum ES modules ada, setiap tag <script> menumpahkan variabelnya ke namespace global, dan urutan pemuatan menentukan siapa melihat apa. ES modules memperbaikinya dengan menjadikan setiap file sebagai scope tersendiri. Tidak ada yang bocor keluar kecuali kamu melakukan export secara eksplisit. Tidak ada yang masuk kecuali kamu melakukan import secara eksplisit.

Dua file, satu melakukan export, satu melakukan import:

add dan multiply berada di math.js. Keduanya hanya terlihat di main.js karena ada import. Tidak ada hal lain di math.js, entah helper, konstanta, atau apa pun, yang bisa dijangkau dari luar.

Dua aturan muncul dari sini dan layak kamu pahami sejak awal:

  • Module otomatis berjalan dalam strict mode. Tidak perlu 'use strict'.
  • this di tingkat atas bernilai undefined, bukan objek global.

Named Export: Export Sambil Jalan

Bentuk yang paling umum. Taruh export di depan function, class, const, atau let mana pun, maka ia menjadi bagian dari permukaan publik module tersebut:

Nama di dalam kurung kurawal harus persis sama dengan nama yang di-export. import { circlearea } akan gagal. Kalau sebuah nama bentrok dengan sesuatu yang sudah kamu punya, ganti namanya saat import dengan as:

Kamu juga bisa mendaftarkan export di bagian bawah file, bukan langsung di tempat. Sebagian orang lebih suka cara ini karena ada bagian "API publik" yang jelas:

Kedua gaya memberi hasil yang sama. Pilih satu dan tetap konsisten dalam satu proyek.

Default Export: Satu per Module

Sebuah module juga bisa punya satu export default. Default ditujukan untuk file yang memang punya satu hal utama, seperti sebuah komponen, class, atau objek konfigurasi:

Tiga hal yang perlu diperhatikan:

  • Tidak ada kurung kurawal di sekitar log di sisi import.
  • Nama yang di-import bebas kamu tentukan. import shout from './logger.js' akan bekerja persis sama.
  • Hanya satu default export per file. Coba tambahkan yang kedua, maka file itu tidak akan bisa di-parse.

Named export dan default export bisa ada bersamaan:

Default dulu, lalu named export di dalam kurung kurawal. Urutannya tetap.

Mana yang sebaiknya kamu pakai? Named export lebih mudah di-refactor: mengganti nama di seluruh codebase cukup dengan satu find-and-replace karena setiap import memakai nama yang sama. Default lebih fleksibel, tetapi setiap pemanggil bisa memilih nama berbeda, sehingga pencarian dengan grep jadi lebih sulit. Kebanyakan panduan gaya modern condong ke named export dan menyisakan default untuk module yang benar-benar hanya punya satu tujuan.

Import Semuanya, Re-Export, dan Efek Samping

Beberapa bentuk import lain yang akan kamu temui.

Ambil semua named export ke dalam satu objek namespace:

Re-export dari module lain tanpa meng-import ke scope saat ini:

Pola itulah cara titik masuk sebuah library mengumpulkan bagian-bagian dari file internal ke satu permukaan publik.

Dan terakhir, import tanpa binding sama sekali, untuk module yang tugasnya hanya menghasilkan efek samping (polyfill, CSS-in-JS, mendaftarkan handler):

File itu berjalan sekali; tidak ada yang di-import berdasarkan nama.

Import Itu Statis dan Hidup

Dua sifat import yang sesekali mengejutkan orang.

Statis. Deklarasi import diselesaikan sebelum kodemu berjalan. Kamu tidak bisa menaruhnya di dalam if, fungsi, atau try. Path-nya harus berupa string literal, bukan variabel. Itulah yang memungkinkan tools menganalisis import tanpa menjalankan kodenya: bundler, type checker, dan tree-shaker semuanya mengandalkan hal ini.

// Not allowed - SyntaxError.
if (userWantsFancy) {
  import { fancy } from './fancy.js';
}

Kalau kamu butuh pemuatan kondisional, pakai import() (dibahas berikutnya).

Hidup. Binding hasil import adalah referensi read-only ke export-nya, bukan salinan. Kalau module yang meng-export mengisi ulang nilainya, module yang meng-import melihat nilai yang baru:

Kamu juga tidak bisa mengisi ulang sebuah import di sisi pemakai: count = 5 di main.js akan memunculkan error. Import adalah tampilan yang read-only.

import() Dinamis untuk Memuat Sesuai Kebutuhan

Saat kamu perlu memutuskan ketika runtime apakah sebuah module dimuat, misalnya untuk fitur yang berat, code splitting berdasarkan route, atau polyfill kondisional, pakai import() sebagai fungsi. Ia mengembalikan promise yang menghasilkan export milik module tersebut:

Karena ini pemanggilan fungsi biasa, kamu bisa:

  • Melakukan await di dalam fungsi async.
  • Memberikan variabel sebagai path-nya.
  • Memakainya di dalam if atau try/catch.

Destructuring pada objek hasilnya bekerja sama seperti import statis:

default adalah key untuk default export saat kamu melakukan destructuring. Ganti namanya sesukamu.

Kasus penggunaan praktisnya adalah code-splitting (library chart baru dikirim saat pengguna mengklik "show chart"), polyfill berdasarkan deteksi fitur, dan plugin yang ditemukan saat runtime.

Menjalankan ES Modules: Browser dan Node

Sintaksnya sama di mana-mana; yang berbeda adalah cara runtime menemukan dan memuat file-nya.

Di browser, tandai script utamanya sebagai module:

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

Dengan type="module", browser menghormati import/export, menjalankan kode dalam strict mode, dan menunda eksekusi sampai HTML selesai di-parse. Path harus relatif (./, ../) atau URL absolut. Bare specifier seperti import 'lodash' tidak berfungsi tanpa import map atau bundler.

Di Node, ada dua cara untuk mengaktifkannya:

  • Beri nama file dengan ekstensi .mjs, atau
  • Atur "type": "module" di package.json terdekat, yang menjadikan setiap file .js sebagai module.

Node juga mewajibkan path lengkap dengan ekstensi: import './utils.js', bukan import './utils'.

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

Kedua lingkungan mewajibkan ekstensi eksplisit di ESM native. Bundler (Vite, webpack, esbuild) akan menyelesaikan path tanpa ekstensi untukmu selama development. Praktis, tetapi mengandalkannya berarti source code-mu tidak akan jalan tanpa langkah build.

Jebakan Umum

Beberapa hal yang sering menjebak:

  • Lupa type="module" di browser. Tanpa itu, <script> berjalan sebagai script klasik dan import menjadi syntax error.
  • Ekstensi file tidak ada di Node. import './utils' gagal; import './utils.js' berhasil. Bundler menyembunyikan masalah ini, runtime native tidak.
  • Mengharapkan __dirname atau require di ES module. Keduanya hanya ada di CommonJS. Di ESM, pakai import.meta.url dan ubah saat kamu butuh path.
  • Import melingkar yang menyentuh nilai sebelum siap. Dua module yang saling meng-import itu sah, tetapi membaca export yang belum diisi akan memberimu undefined. Susun kodemu agar putaran itu tidak tersentuh saat inisialisasi, atau pecahlah.
  • Mencoba melakukan import secara kondisional. Pernyataan import statis tidak mengizinkannya. Pakai import() dinamis untuk apa pun yang bergantung pada runtime.

Berikutnya: CommonJS vs ESM

ES modules adalah standarnya, tetapi banyak kode Node di lapangan masih memakai CommonJS, dengan require, module.exports, dan aturan yang berbeda soal kapan kode berjalan. Memahami keduanya, dan cara keduanya saling bekerja sama, adalah topik halaman berikutnya.

Pertanyaan yang sering diajukan

Bagaimana cara memakai ES modules di JavaScript?

Export nilai dari satu file dengan export atau export default, lalu ambil ke file lain dengan import. Di browser, muat file utamanya dengan <script type="module" src="main.js"></script>. Di Node, pakai ekstensi .mjs atau atur "type": "module" di package.json.

Apa bedanya default export dan named export?

Sebuah module bisa punya banyak named export (export function foo() {}) tetapi hanya satu default export (export default ...). Named export harus di-import dengan nama yang persis sama di dalam kurung kurawal: import { foo } from './x.js'. Default export bisa di-import dengan nama apa pun: import whatever from './x.js'.

Apa itu import() dinamis di JavaScript?

import() yang dipanggil sebagai fungsi mengembalikan promise yang menghasilkan export milik module tersebut. Berbeda dengan pernyataan import statis, ia berjalan saat dipanggil, jadi kamu bisa memuat kode secara kondisional atau sesuai kebutuhan. Begitulah cara menerapkan code-splitting dan lazy loading.

Apakah path import harus memakai ekstensi file?

Di ES modules native, yaitu browser dan loader ESM milik Node, ya. Kamu harus menulis ./utils.js, bukan ./utils. Bundler seperti Vite dan webpack lebih toleran dan akan menyelesaikan path tanpa ekstensi untukmu, tetapi mengandalkannya membuat kodemu tidak portabel.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai