ES modülleri, JavaScript kodunu dosyalara bölüp aralarında değer paylaşmanın standart yoludur: bir dosya export ile dışarıya açtığı fonksiyon, sınıf veya değişkenleri belirtir, diğer dosya import ile bunları alır. Bir modülde birden çok adlandırılmış (named) export ve en fazla bir varsayılan (default) export olabilir; kodu ihtiyaç anında yüklemek için de import() ifadesi kullanılır.

Bu yazıda sözdizimini, modül kapsamının sıradan betiklerden farkını ve dinamik içe aktarmayı Node.js 24 üzerinde çalıştırılmış örneklerle anlatıyoruz. Node.js'in genel yapısını merak ediyorsanız Node.js nedir yazımıza göz atabilirsiniz.

Dosya uzantısı: .mjs mi .js mi?

MDN rehberi, modül dosyaları için iki uzantının da kullanıldığını belirtir. .mjs uzantısı hangi dosyanın modül olduğunu açıkça gösterir ve Node.js gibi çalışma ortamlarının dosyayı modül olarak ayrıştırmasını garanti eder. Tarayıcıda ise uzantıdan çok sunucunun dosyayı text/javascript gibi bir JavaScript MIME türüyle göndermesi önemlidir; aksi halde "strict MIME type" hatası alırsınız. Bu yazıdaki örnekler doğrudan Node.js ile çalıştırılabilsin diye .mjs uzantısı kullanıyor.

Adlandırılmış ve varsayılan export

Adlandırılmış ve varsayılan export: Adlandırılmış, Yeniden adlandırma, Varsayılan, Ad alanı

export anahtar kelimesini bir bildirimin önüne yazabilir ya da dosyanın sonunda export { a, b } listesiyle toplu dışa aktarım yapabilirsiniz. as ile dışarıya farklı bir isimle açmak da mümkündür. Export'lar yalnızca modülün en üst seviyesinde yazılabilir, bir fonksiyonun içinde kullanılamaz.

javascript
// matematik.mjs
export const PI_YAKLASIK = 3.14;

export function kare(x) {
  return x * x;
}

function kup(x) {
  return x * x * x;
}
export { kup as ucuncuKuvvet };

export default function topla(a, b) {
  return a + b;
}

Varsayılan export'u içe aktarırken süslü parantez kullanmazsınız ve istediğiniz ismi verebilirsiniz. Adlandırılmış export'lar ise aynı isimle (gerekirse as ile yeniden adlandırarak) alınır. import * as M biçimi tüm export'ları tek bir modül ad alanı nesnesinde toplar; varsayılan export bu nesnede default anahtarıyla durur.

javascript
// main.mjs  (çalıştırma: node main.mjs)
import topla, { kare, PI_YAKLASIK, ucuncuKuvvet as kup } from "./matematik.mjs";
import * as M from "./matematik.mjs";

console.log(topla(2, 3)); // 5
console.log(kare(4), kup(2)); // 16 8
console.log(PI_YAKLASIK); // 3.14
console.log(Object.keys(M)); // [ 'PI_YAKLASIK', 'default', 'kare', 'ucuncuKuvvet' ]
console.log(M.default === topla); // true

Modül ad alanı nesnesi prototipi null olan, mühürlenmiş (sealed) bir nesnedir ve anahtarları sözlük sırasıyla listelenir; çıktıdaki sıralama bu yüzden alfabetik. Göreli yollarda dosya uzantısını yazmayı unutmayın: tarayıcılar ve Node.js'in ESM uygulaması, "./matematik" gibi uzantısız bir belirteci CommonJS'teki gibi otomatik tamamlamaz.

BiçimSözdizimiNe alınır
Adlandırılmışimport { a, b } from "./m.mjs"Belirtilen isimli export'lar
Yeniden adlandırmaimport { a as x } from "./m.mjs"a, x adıyla
Varsayılanimport ad from "./m.mjs"default export, istediğiniz isimle
Ad alanıimport * as M from "./m.mjs"Tüm export'lar M nesnesinde
Yan etkiimport "./m.mjs"Hiçbir değer; yalnızca modül çalışır
Dinamikawait import("./m.mjs")Promise ile ad alanı nesnesi

İçe aktarılan değerler canlı ve salt okunurdur

İçe aktarılan değerler canlı ve salt okunurdur

import bir kopya değil, canlı bağlantı (live binding) oluşturur. Dışa aktaran modül değişkeni değiştirirse içe aktaran taraf yeni değeri görür. Buna karşılık içe aktaran modül bu değişkene yeni değer atayamaz; davranış const ile aynıdır. Nesne türündeki değerlerin özelliklerini değiştirmek ise mümkündür.

javascript
// sayac.mjs
export let sayi = 0;
export function artir() {
  sayi++;
}

// main2.mjs
import { sayi, artir } from "./sayac.mjs";

console.log(sayi); // 0
artir();
console.log(sayi); // 1
try {
  sayi = 5;
} catch (e) {
  console.log(e.constructor.name, e.message); // TypeError Assignment to constant variable.
}

Modül kapsamı ve klasik betiklerden farklar

Bir modülün en üst seviyesinde tanımladığınız değişkenler o modüle aittir; var ile tanımlasanız bile global nesneye eklenmez. Modüller otomatik olarak strict mode ile çalışır ve en üst seviyedeki this değeri undefined olur. Global değişkenlere (globalThis, console gibi) ise modül içinden erişebilirsiniz. Bir modül, kaç kez içe aktarılırsa aktarılsın yalnızca bir kez çalıştırılır.

javascript
// kapsam.mjs
var gizli = 1;
console.log(globalThis.gizli); // undefined (modül kapsamında kaldı)
console.log(this); // undefined

function kim() {
  return this;
}
console.log(kim()); // undefined (strict mode)

Kapsam kurallarının genel hâli için scope yazısına, strict mode'un getirdiği diğer farklar için strict mode yazısına bakabilirsiniz.

Import bildirimleri hoisting'e tabidir: modül kodunun geri kalanı çalışmadan önce bağımlı modüller yüklenir ve çalıştırılır. Yine de okunabilirlik için tüm import'ları dosyanın başına yazmak iyi bir alışkanlıktır. Import bildirimi yalnızca modülün en üst seviyesinde yazılabilir; bir if bloğu veya fonksiyon içinde kullanılamaz.

Dinamik import() ile ihtiyaç anında yükleme

import(belirtec) fonksiyon çağrısına benzeyen ama aslında bir anahtar kelime olan bir ifadedir. Modülü yükleyip çalıştırır ve modül ad alanı nesnesiyle yerine gelen bir Promise döndürür. Belirteç herhangi bir ifade olabilir, koşullu olarak çağrılabilir ve modül olmayan betiklerde de kullanılabilir. Yükleme başarısız olursa Promise reddedilir; import() hiçbir zaman senkron olarak hata fırlatmaz.

javascript
// dinamik.mjs  (modüllerde en üst seviyede await kullanılabilir)
const gerekli = true;
if (gerekli) {
  const { default: topla, kare } = await import("./matematik.mjs");
  console.log(topla(1, 1), kare(3)); // 2 9
}

try {
  await import("./olmayan.mjs");
} catch (e) {
  console.log("Yüklenemedi:", e.code); // Yüklenemedi: ERR_MODULE_NOT_FOUND
}

default ayrılmış bir kelime olduğu için ad alanı nesnesini parçalarken (destructuring) onu yeniden adlandırmanız gerekir. Hata nesnesinin türü ortama göre değişir: MDN'e göre Node.js genel bir Error, tarayıcılar TypeError kullanır; yukarıdaki code özelliği Node.js'e özgüdür. Parçalama sözdizimi için destructuring yazısına bakabilirsiniz.

Modül olmayan bir betikten de ESM dosyası yükleyebilirsiniz. Aşağıdaki komut, node -e ile çalışan sıradan bir betikten matematik.mjs dosyasını yüklüyor:

bash
node -e "import('./matematik.mjs').then((m) => console.log(m.kare(5)))"
# 25

Yeniden dışa aktarma (barrel modül)

Birden çok alt modülün export'larını tek bir giriş noktasında toplamak için export { Ad } from "./alt.mjs" veya export * from "./alt.mjs" biçimleri kullanılır. Bu satırlar değerleri yalnızca yönlendirir; aynı dosyada o isimleri yerel değişken olarak kullanamazsınız.

javascript
// sekiller.mjs
export { Kare } from "./sekiller/kare.mjs";
export { Daire } from "./sekiller/daire.mjs";

// main.mjs
import { Kare, Daire } from "./sekiller.mjs";

Yaygın hatalar

Yaygın hatalar: Varsayılan ile adlandırılmışı karıştırmak, Aynı ismi iki kez içe aktarmak, Döngüsel bağımlılık, Modülden then adlı fonksiyon export etmek
  • Varsayılan ile adlandırılmışı karıştırmak: import { topla } from "./matematik.mjs", varsayılan export'u almaz. MDN'in vurguladığı gibi, adlandırılmış import ile varsayılan nesneyi parçalamak aynı şey değildir.
  • Aynı ismi iki kez içe aktarmak: İki modülden draw adlı iki export alırsanız isim çakışır. as ile yeniden adlandırın veya import * as ile ad alanı kullanın.
  • Döngüsel bağımlılık: a.mjs ile b.mjs birbirini içe aktarıyorsa ve biri diğerinin henüz başlatılmamış değişkenini senkron olarak okursa ReferenceError alırsınız. Ortak kodu üçüncü bir modüle taşımak en temiz çözümdür.
  • Modülden then adlı fonksiyon export etmek: Dinamik import'un döndürdüğü Promise, ad alanı nesnesini thenable sanıp bu fonksiyonu çağırır. MDN bunu açıkça önermez.
  • Tarayıcıda file:// ile test etmek: Modüller CORS kurallarına tabidir; sayfayı yerel bir sunucu üzerinden açın.

Sık Sorulan Sorular

Bir modülde kaç tane default export olabilir?

En fazla bir. Aynı isimde iki export veya birden fazla default yazmak SyntaxError üretir ve modül hiç çalışmaz.

import ile içe aktardığım değeri neden değiştiremiyorum?

İçe aktarılan isim, dışa aktaran modüldeki değişkene canlı ve salt okunur bir bağlantıdır. Değeri yalnızca onu dışa aktaran modül değiştirebilir; siz bunun için o modülün açtığı bir fonksiyonu çağırmalısınız.

Statik import'u if içinde kullanabilir miyim?

Hayır, import bildirimleri yalnızca en üst seviyede yazılabilir. Koşullu yükleme için await import(...) kullanın.

Modüller Promise ile nasıl ilişkili?

import() bir Promise döndürür ve modüllerde en üst seviyede await kullanılabilir. Promise ve async yapısını async/await yazısında anlattık.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Node.js nedir? · async/await · Scope (kapsam) · Strict mode · JavaScript class kullanımı · Node.js ile REST API