Modül Dediğimiz Şey: Kendi Kapsamı Olan Bir Dosya
ES modüllerinden önce her <script> etiketi değişkenlerini global alana boca ediyordu; hangi dosyanın neyi gördüğünü de yükleme sırası belirliyordu. ES modülleri bu kaosu çözüyor: artık her dosyanın kendi kapsamı (scope) var. Açıkça export etmediğiniz hiçbir şey dışarı sızmaz; import etmediğiniz hiçbir şey de içeri girmez.
İki dosya — biri export eden, diğeri import eden:
add ve multiply, math.js dosyasında yaşıyor. Bunların main.js içinden görünür hale gelmesinin tek nedeni import satırı. math.js içindeki diğer her şey — yardımcı fonksiyonlar, sabitler, ne varsa — dışarıdan erişilemez.
Buradan çıkan ve erkenden içselleştirmekte fayda olan iki kural var:
- Modüller otomatik olarak strict mode'da çalışır. Ayrıca
'use strict'yazmana gerek yok. - En üst seviyedeki
this, global nesne değil;undefined'dır.
Named export: yazarken dışa aktar
En yaygın kullanılan biçim. Herhangi bir function, class, const veya let ifadesinin başına export koyduğun anda o şey modülün dışa açık yüzünün bir parçası haline gelir:
Süslü parantez içindeki isimler, dışa aktarılan isimlerle bire bir aynı olmak zorunda — yani import { circlearea } yazarsan hata alırsın. Eğer isim, kodunda zaten kullandığın bir şeyle çakışıyorsa as ile içe aktarırken yeniden adlandırabilirsin:
Export'ları satır içinde yazmak yerine dosyanın en altında toplu halde listeleyebilirsin. Pek çok geliştirici, dosyanın "dışa açılan API"sini tek bir yerde görmek için bu yaklaşımı tercih ediyor:
Her iki yazım şekli de aynı sonucu verir. Bir projede birini seçip ona sadık kalın.
Default Export: Modül Başına Bir Tane
Bir modül, tek bir default export da içerebilir. Default export, gerçekten tek bir ana şey ihraç eden dosyalar için uygundur — bir bileşen, bir sınıf ya da bir konfigürasyon nesnesi gibi:
Dikkat edilmesi gereken üç nokta:
- Import tarafında
logetrafında süslü parantez yok. - İçe aktardığınız isim size kalmış.
import shout from './logger.js'yazsanız da aynı şekilde çalışır. - Her dosyada yalnızca bir tane default export olabilir. İkincisini eklemeye kalkarsanız dosya parse bile edilmez.
Named export ile default export aynı dosyada bir arada kullanılabilir:
Önce default, sonra süslü parantez içinde named export'lar. Sıra sabit, değişmiyor.
Peki hangisini kullanmalısınız? Named export'ları refactor etmek çok daha kolay — kod tabanında bir ismi değiştirmek tek bir "bul ve değiştir" işine bakar, çünkü her import aynı ismi kullanır. Default export'lar esneklik sağlar ama her çağıranın farklı bir isim seçmesine izin verdiği için grep ile aramayı zorlaştırır. Modern stil kılavuzlarının çoğu named export'u tercih ediyor; default export'u ise gerçekten tek amaçlı modüller için saklıyor.
Hepsini import etmek, yeniden export etmek ve side-effect import'lar
Karşınıza çıkacak birkaç import kalıbı daha var.
Tüm named export'ları tek bir namespace nesnesine toplamak için:
Başka bir modülden import etmeden doğrudan yeniden dışa aktarmak (re-export) için şunu kullanabilirsiniz:
Bu kalıp, kütüphanelerin giriş noktalarının iç dosyalardaki parçaları tek bir dışa açık yüzeyde toplamak için kullandığı yöntemdir.
Ve son olarak, hiçbir bağlama almadan yapılan import — yani tek işi yan etki üretmek olan modüller için (polyfill'ler, CSS-in-JS, handler kaydı vb.):
Dosya bir kez çalışır; hiçbir şey isimle import edilmez.
Import'lar Statik ve Canlıdır
import ifadesinin bazen insanları şaşırtan iki özelliği var.
Statik. import bildirimleri, kodunuzun herhangi bir satırı çalışmadan önce çözümlenir. Yani bir if bloğunun, bir fonksiyonun ya da try bloğunun içine import koyamazsınız. Yol kısmı da mutlaka string literal olmak zorunda, değişken kabul etmez. Araçların kodu çalıştırmadan import'ları analiz edebilmesinin sebebi tam olarak bu — bundler'lar, tip kontrolcüleri, tree-shaking yapan araçlar hep bu davranışa güveniyor.
// İzin verilmez — SyntaxError.
if (userWantsFancy) {
import { fancy } from './fancy.js';
}
Koşullu yükleme gerekiyorsa import() kullanın (birazdan göreceğiz).
Canlı bağlantı. İçe aktarılan bir bağ, değerin o anki kopyası değil; ihracatçı modüle geri giden salt okunur bir referanstır. Dışa aktaran modül değeri yeniden atarsa, içe aktaranlar yeni değeri görür:
Tüketici tarafında bir import'u yeniden atayamazsınız da — main.js içinde count = 5 yazmak hata fırlatır. Import'lar salt-okunur görünümlerdir.
İhtiyaç Anında Yüklemek için Dynamic import()
Çalışma zamanında bir modülü yükleyip yüklememeye karar vermeniz gereken durumlarda — ağır özellikler, route bazlı code splitting, koşullu polyfill'ler — import()'ı fonksiyon olarak kullanın. Geriye modülün export'larına resolve olan bir promise döner:
Bu normal bir fonksiyon çağrısı olduğu için şunları yapabilirsin:
asyncbir fonksiyonun içindeawaitile bekletebilirsin.- Yol olarak bir değişken geçirebilirsin.
ifya datry/catchbloklarının içinde kullanabilirsin.
Dönen nesneyi destructuring ile almak, statik import ile tamamen aynı şekilde çalışır:
default, default export'u destructure ettiğinde kullanılan anahtardır. İstediğin isimle yeniden adlandırabilirsin.
Pratikte en çok işe yaradığı yerler: code splitting (kullanıcı "grafiği göster"e tıklamadan chart kütüphanesini hiç yüklememek), feature detection ile polyfill yükleme ve runtime'da keşfedilen eklentiler.
ES Modules'ü Çalıştırmak: Tarayıcı ve Node.js
Sözdizimi her yerde aynı; değişen tek şey, runtime'ın dosyayı nasıl bulup yüklediği.
Tarayıcıda, giriş script'ini modül olarak işaretlemen yeterli:
<script type="module" src="./main.js"></script>
type="module" ile script etiketini kullandığında tarayıcı import/export ifadelerini tanır, kodu strict mode'da çalıştırır ve HTML parse edilene kadar yürütmeyi erteler. Yol belirtirken ya göreli (./, ../) ya da mutlak URL vermelisin; import 'lodash' gibi çıplak (bare) specifier'lar import map veya bir bundler olmadan çalışmaz.
Node tarafında ES modules'a geçmek için iki yol var:
- Dosyayı
.mjsuzantısıyla kaydet, ya da - En yakın
package.jsondosyasına"type": "module"ekle; böylece tüm.jsdosyaları modül olarak değerlendirilir.
Ayrıca Node, uzantıyla birlikte tam yol ister: import './utils' değil, import './utils.js' yazmalısın.
// package.json
{
"type": "module",
"main": "./index.js"
}
Her iki ortam da native ESM kullanırken dosya uzantısını açıkça yazmanızı ister. Bundler'lar (Vite, webpack, esbuild) geliştirme sırasında uzantısız yolları sizin için çözer — pratik, evet; ama buna bel bağlarsanız kaynak kodunuz build adımı olmadan çalışmaz.
Sık Yapılan Hatalar
İnsanların takıldığı birkaç nokta:
- Tarayıcıda
type="module"yazmayı unutmak. Bunu eklemezseniz<script>klasik script olarak çalışır veimportbir syntax error'a dönüşür. - Node'da dosya uzantısını atlamak.
import './utils'çalışmaz;import './utils.js'çalışır. Bundler'lar bunu gizler, native runtime'lar gizlemez. - ES modülünün içinde
__dirnameveyarequirebeklemek. Bunlar yalnızca CommonJS'e özel. ESM'deimport.meta.urlkullanın ve yol lazımsa onu dönüştürün. - Değerlere hazır olmadan dokunan döngüsel import'lar. İki modülün birbirini import etmesi teknik olarak geçerlidir; ama henüz atanmamış bir export'u okursanız elinize
undefinedgeçer. Kodu, başlatma sırasında döngüye girilmeyecek şekilde düzenleyin ya da modülleri bölün. - Koşullu
importdenemek. Statikimportifadesi buna izin vermez. Runtime'a bağlı her şey için dynamicimport()kullanın.
Sırada: CommonJS vs ESM
ES modülleri artık standart, ama piyasadaki pek çok Node kodu hâlâ CommonJS kullanıyor — require, module.exports ve kodun ne zaman çalıştığına dair bambaşka kurallar. İkisini de bilmek ve nasıl birlikte çalıştıklarını anlamak bir sonraki sayfanın konusu.
Sıkça Sorulan Sorular
JavaScript'te ES modules nasıl kullanılır?
Bir dosyadan değerleri export veya export default ile dışa aktarırsınız, sonra başka bir dosyada import ile içeri alırsınız. Tarayıcıda giriş dosyasını <script type="module" src="main.js"></script> şeklinde yüklemeniz gerekir. Node tarafında ise ya .mjs uzantısı kullanırsınız ya da package.json dosyanıza "type": "module" eklersiniz.
Default export ile named export arasındaki fark nedir?
Bir modülde istediğiniz kadar named export olabilir (export function foo() {}), ama sadece tek bir default export bulunabilir (export default ...). Named export'ları süslü parantez içinde ve aynı isimle import etmek zorundasınız: import { foo } from './x.js'. Default export'u ise istediğiniz isimle alabilirsiniz: import neOlursa from './x.js'.
JavaScript'te dynamic import() nedir?
import() fonksiyon gibi çağrıldığında modülün export'larına resolve olan bir promise döner. Statik import ifadesinin aksine çağrıldığı anda çalışır; yani kodu koşullu olarak veya ihtiyaç anında yükleyebilirsiniz. Code splitting ve lazy loading işte tam olarak bu şekilde yapılır.
Import yollarında dosya uzantısı yazmak zorunda mıyım?
Native ES modules kullanıyorsanız (tarayıcılar ve Node'un ESM loader'ı) evet, zorundasınız. ./utils değil, ./utils.js yazmanız gerekir. Vite veya webpack gibi bundler'lar bu konuda daha esnek davranır ve uzantısız yolları sizin yerinize çözer — ama buna güvenmek kodunuzu taşınabilir olmaktan çıkarır.