Mert Özen Her Satırda Daha İleri
Frontend JavaScript İleri — #13

JavaScript ile Dosya ve İkili Veri: File, Blob, ArrayBuffer ve Typed Array'ler

Mert Özen 3 Kas 2026 10 dk 5 görüntülenme
JavaScript ile Dosya ve İkili Veri: File, Blob, ArrayBuffer ve Typed Array'ler

Kullanıcıdan input ve sürükle-bırak ile dosya alıyor, Blob ve File'ı okuyor, ArrayBuffer, typed array ve DataView ile byte'lara iniyor, ES2026'nın base64/hex method'larını ve dosya indirmeyi öğreniyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bu yazının kahramanlarından biriyle başlayalım. Dört sayıdan oluşan bir array oluşturuyoruz, ama düz bir array değil:

const bytes = new Uint8Array([255, 256, 257, -1]);
console.log(bytes);

Bu kod konsola ne yazdırır? Dört sayı aynen mi kalır, bir hata mı alırız, yoksa başka bir şey mi olur? Okumaya devam etmeden önce bir tahmin yap. Cevap, typed array'lerin byte'larla nasıl çalıştığını gördüğümüzde kendiliğinden ortaya çıkacak.

Blob ve File: Ham Verinin Kutusu

Şimdiye kadar çalıştığımız veriler hep string, number, object gibi JavaScript değerleriydi. Kullanıcının bilgisayarından gelen bir fotoğraf, PDF ya da ses kaydı ise sadece bir byte yığınıdır. Tarayıcı bu yığını Blob (Binary Large Object) adlı bir object'e koyar.

Blob'u kapalı bir kargo kutusu gibi düşün. Üzerinde ağırlığı (size) ve etiketi (type) yazar, ama içini görmek için kutuyu açman gerekir ve açmak zaman alır. Bu yüzden içeriği okuyan method'lar promise döndürür. Bunu tarayıcının konsoluna yapıştırıp dene:

const blob = new Blob(["Hello ", "café"], { type: "text/plain" });
console.log(blob.size, blob.type); // 11 "text/plain"

const text = await blob.text();
console.log(text); // "Hello café"

"Hello café" 10 karakter ama size 11 diyor. Çünkü size karakter değil byte sayar; string'ler Blob'a UTF-8 olarak yazılır ve "é" UTF-8'de iki byte tutar. Kutudan veriyi üç şekilde çıkarabilirsin: text() metin olarak, arrayBuffer() ham byte olarak, stream() ise daha önce gördüğümüz ReadableStream olarak, parça parça. slice(start, end) kutuyu açmadan bir bölümünü yeni bir Blob olarak keser; büyük dosyanın sadece başına bakmak istediğinde çok işe yarar.

File ise Blob'un bir alt türüdür. Blob'un her şeyine sahiptir, üstüne name ve lastModified ekler. Kullanıcının seçtiği her dosya sana bir File olarak gelir, yani Blob için öğrendiğin her şey dosyalar için de geçerli.

Kullanıcıdan Dosya Almak

Tarayıcı güvenlik gereği dosya sistemine kendi kendine erişemez; dosyayı kullanıcının vermesi gerekir. En basit yol <input type="file">. Tam bir örnek:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Reading files</title>
</head>
<body>
  <input type="file" id="picker" accept=".txt,.csv,.json" multiple>
  <pre id="output"></pre>
  <script>
    const picker = document.getElementById("picker");
    const output = document.getElementById("output");

    picker.addEventListener("change", async () => {
      output.textContent = "";
      for (const file of picker.files) {
        const text = await file.text();
        const kb = (file.size / 1024).toFixed(1);
        output.textContent += `${file.name} (${kb} KB, ${file.type || "unknown type"})\n`;
        output.textContent += text.slice(0, 200) + "\n\n";
      }
    });
  </script>
</body>
</html>

picker.files bir FileList; array değil ama for...of ile gezilebilir. accept sadece seçim penceresindeki filtreyi belirler, kullanıcı yine de başka bir dosya seçebilir. O yüzden gelen dosyayı her zaman kendin kontrol et.

Eski kodlarda aynı işi FileReader ile yapıldığını görürsün:

const reader = new FileReader();
reader.addEventListener("load", () => console.log(reader.result));
reader.addEventListener("error", () => console.error(reader.error));
reader.readAsText(file);

FileReader event tabanlı çalışır ve readAsText, readAsArrayBuffer, readAsDataURL gibi method'ları vardır. Hâlâ her yerde destekleniyor, ama yeni kodda promise döndüren text() ve arrayBuffer() hem daha kısa hem async/await ile daha doğal.

Sürükle-Bırak ve Önizleme

Dosyayı bir alana sürükleyip bırakmak, kullanıcıya input'tan daha rahat gelir. Aşağıdaki dosyada bir bırakma alanı ve görsel önizlemesi var:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Drag and drop</title>
  <style>
    #zone { border: 3px dashed #888; padding: 40px; text-align: center; }
    #zone.over { border-color: #2563eb; background: #eff6ff; }
    #preview img { max-width: 200px; margin: 8px; }
  </style>
</head>
<body>
  <div id="zone">Drop images here</div>
  <div id="preview"></div>
  <script>
    const zone = document.getElementById("zone");
    const preview = document.getElementById("preview");

    zone.addEventListener("dragover", (event) => {
      event.preventDefault(); // bunu yapmazsan drop hiç gelmez
      zone.classList.add("over");
    });
    zone.addEventListener("dragleave", () => zone.classList.remove("over"));

    zone.addEventListener("drop", (event) => {
      event.preventDefault(); // tarayıcının dosyayı açmasını engeller
      zone.classList.remove("over");

      for (const file of event.dataTransfer.files) {
        if (!file.type.startsWith("image/")) continue;
        const img = document.createElement("img");
        img.src = URL.createObjectURL(file);
        img.alt = file.name;
        img.addEventListener("load", () => URL.revokeObjectURL(img.src));
        preview.append(img);
      }
    });
  </script>
</body>
</html>

İki preventDefault() da şart. dragover'da varsayılanı engellemezsen tarayıcı bu alanı geçerli bir bırakma hedefi saymaz; drop'ta engellemezsen bıraktığın görseli yeni sekmede açar ve sayfan gider. Bırakılan dosyalar event.dataTransfer.files içinde, yine bir FileList olarak gelir.

Önizlemenin sırrı URL.createObjectURL(file). Bu method blob: ile başlayan, sadece bu sayfada geçerli kısa bir adres üretir. Adres dosyanın kopyası değil, bellekteki Blob'u gösteren bir işaretçidir; o yüzden çok büyük bir görsel için bile anında hazır olur. Bedeli şu: sen URL.revokeObjectURL() diyene kadar tarayıcı o Blob'u bellekte tutar. Görsel yüklendikten sonra adresi bırakıyoruz. Alternatif olarak readAsDataURL ile görseli base64 metne çevirebilirsin, ama bu metin dosyadan yaklaşık üçte bir daha büyük olur ve önizleme için gereksiz bir yüktür.

ArrayBuffer ve Typed Array'ler

Blob'un içini byte byte okumak istediğinde arrayBuffer() sana bir ArrayBuffer verir. ArrayBuffer sabit uzunlukta, ham bir bellek bloğudur ve tek başına okunamaz. Ona bakmak için bir "gözlük" takman gerekir; bu gözlüklerin adı typed array.

Yan yana dizilmiş dört kutu düşün. Uint8Array gözlüğü her kutuya ayrı ayrı bakar ve her birinde 0 ile 255 arasında bir sayı görür. Uint16Array gözlüğü kutulara ikişer ikişer bakar, her çifti tek bir sayı olarak okur. Kutular aynı, bakış farklı. Bunu konsolda dene:

const buffer = new ArrayBuffer(4);
const bytes = new Uint8Array(buffer);
const words = new Uint16Array(buffer);

bytes[0] = 1;
bytes[1] = 2;

console.log(words[0]); // 513
console.log(buffer.byteLength, bytes.length, words.length); // 4 4 2

bytes üzerinden yazdığımız iki byte, words üzerinden tek bir sayı olarak okunuyor: 1 + 2 × 256 = 513. Düşük değerli byte'ın önce gelmesine little-endian denir ve bugün kullandığın neredeyse bütün bilgisayarlar böyle çalışır. Bu ayrıntı birazdan önemli olacak.

Aile kalabalık: Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array ve ES2025 ile gelen Float16Array. Hepsi map, filter, slice, includes gibi tanıdık method'ların çoğunu taşır, ama uzunlukları sabittir; push ya da pop yoktur.

Baştaki sorunun cevabı da burada. Bir Uint8Array element'i tek byte'tır ve sadece 0–255 arasını tutabilir. Sığmayan sayı hata vermez, 256'ya göre sarılır: 256 sıfır olur, 257 bir olur, -1 de 255 olur. Konsolda Uint8Array(4) [255, 0, 1, 255] görürsün. Bir kardeşi olan Uint8ClampedArray ise sarmak yerine sınıra yapıştırır: aynı değerler [255, 255, 255, 0] olur. Canvas'ın piksel verisinde bu kardeşle tekrar karşılaşacağız.

Daha önce Web Worker yazısında gördüğümüz transferable objects konusunu hatırla: ArrayBuffer tam olarak transfer edilebilen türlerden biri. worker.postMessage(buffer, [buffer]) dediğinde megabaytlarca veri kopyalanmadan worker'a geçer.

DataView: Bir Dosyanın Başlığını Okumak

Typed array'ler bütün buffer'ı tek tipte görür. Gerçek dosya formatlarında ise başlıkta farklı uzunlukta alanlar art arda gelir: bir yerde 1 byte'lık bir işaret, hemen ardından 4 byte'lık bir sayı. DataView istediğin konumdan istediğin tipte okuma yapmanı sağlar ve sayının byte sırasını da sana bırakır.

Somut bir örnek: bir PNG dosyasının genişlik ve yüksekliğini, görseli hiç çizmeden, sadece ilk 24 byte'ına bakarak bulalım. PNG dosyaları sabit bir imzayla başlar ve genişlik 16., yükseklik 20. byte'tan itibaren 4 byte'lık sayılar olarak yazılır:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>PNG size</title>
</head>
<body>
  <input type="file" id="picker" accept="image/png">
  <p id="result"></p>
  <script>
    async function pngSize(file) {
      const buffer = await file.slice(0, 24).arrayBuffer();
      const view = new DataView(buffer);
      const signature = [0x89, 0x50, 0x4e, 0x47]; // "\x89PNG"

      for (let i = 0; i < signature.length; i++) {
        if (view.getUint8(i) !== signature[i]) throw new Error("This is not a PNG");
      }
      return { width: view.getUint32(16), height: view.getUint32(20) };
    }

    document.getElementById("picker").addEventListener("change", async (event) => {
      const result = document.getElementById("result");
      try {
        const { width, height } = await pngSize(event.target.files[0]);
        result.textContent = `${width} × ${height} pixels`;
      } catch (error) {
        result.textContent = error.message;
      }
    });
  </script>
</body>
</html>

file.slice(0, 24) sayesinde 20 MB'lık bir görselin bile sadece 24 byte'ı okunuyor. getUint8(i) tek byte okur, getUint32(16) 16. byte'tan başlayan dört byte'ı tek bir sayı yapar. Burada ince bir nokta var: PNG sayılarını big-endian, yani yüksek değerli byte önce gelecek şekilde saklar ve DataView varsayılan olarak tam da böyle okur. Little-endian bir format okuyacaksan ikinci parametreyi verirsin: view.getUint32(16, true). İmza kontrolü de ayrıca değerli; dosyanın uzantısı ya da type alanı ne derse desin, gerçek türü bu ilk byte'lar söyler.

Base64 ve Hex: ES2026 ile Gelen Kısa Yol

İkili veriyi metin olarak taşıman gereken yerler var: bir JSON alanına koymak, URL'de göndermek, ekrana yazdırmak. Bunun iki yaygın yolu base64 ve hex. ES2026 ile Uint8Array'e bu dönüşümleri doğrudan yapan method'lar geldi. Konsolda dene:

const bytes = new TextEncoder().encode("Hello");
console.log(bytes.toBase64()); // "SGVsbG8="
console.log(bytes.toHex());    // "48656c6c6f"

const decoded = Uint8Array.fromBase64("SGVsbG8=");
console.log(new TextDecoder().decode(decoded)); // "Hello"

TextEncoder bir string'i UTF-8 byte'larına çevirir, TextDecoder byte'ları tekrar string'e. Aradaki köprüyü artık toBase64(), fromBase64(), toHex() ve fromHex() kuruyor. toBase64({ alphabet: "base64url", omitPadding: true }) dersen URL'de sorun çıkarmayan - ve _ karakterleri kullanılır ve sondaki = dolgusu atılır. Geçersiz bir girdi verirsen fromBase64() bir SyntaxError fırlatır.

Bu method'lar neden önemli? Çünkü eski yol olan btoa() sadece Latin-1 karakterlerle çalışır. btoa("Çağrı") bir InvalidCharacterError fırlatır, çünkü "ğ" ve "ı" o aralıkta yok. Yeni yolda new TextEncoder().encode("Çağrı").toBase64() sorunsuz çalışır ve "w4dhxJ9yxLE=" döndürür. Bu method'lar Eylül 2025'ten beri Baseline; Chrome 140, Firefox 133 ve Safari 18.2'den itibaren çalışıyor. Daha eski tarayıcıları da düşünmen gerekiyorsa önce typeof Uint8Array.prototype.toBase64 === "function" ile kontrol et.

Tarayıcıda Dosya Oluşturup İndirmek

Sunucuya hiç gitmeden, tarayıcıda ürettiğin veriyi dosya olarak indirtebilirsin. Tarif hep aynı: bir Blob oluştur, ona bir blob: adresi ver, download attribute'u olan bir link'e tıklat:

function downloadFile(content, fileName, type) {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);

  const link = document.createElement("a");
  link.href = url;
  link.download = fileName;
  link.click();

  setTimeout(() => URL.revokeObjectURL(url), 1000); // indirme başladıktan sonra bırak
}

downloadFile(JSON.stringify({ name: "Ada", score: 42 }, null, 2), "data.json", "application/json");

Konsolda çalıştırdığında tarayıcı data.json adında bir dosya indirir. download attribute'u tarayıcıya "bu adresi açma, kaydet" der ve dosyanın adını belirler. Link'i sayfaya eklemene bile gerek yok. Aynı fonksiyonla CSV de üretebilirsin; Türkçe karakterli bir CSV'yi Excel'in doğru okuması için içeriğin başına "" (UTF-8 BOM) eklemek işe yarayan eski bir numaradır.

Sık Düşülen Tuzaklar

file.type'a güvenme. Bu değer çoğunlukla dosya uzantısından tahmin edilir; uzantısı değiştirilmiş bir dosya seni kandırır, bazı dosyalarda da boş gelir. Türü gerçekten bilmen gerekiyorsa ilk byte'lara bak. Dosya bir sunucuya gidecekse kontrolü orada da tekrarla; tarayıcıdaki kontrol kullanıcı deneyimi içindir, güvenlik için değil.

createObjectURL ile ürettiğin adresleri bırakmamak sessiz bir sızıntıdır. Bellek yönetimi yazısında konuştuğumuz unutulan listener'lar gibi, kimse fark etmez ama yüzlerce görselin önizlendiği bir sayfada bellek şişer. İşin bitince revokeObjectURL() çağır.

Büyük bir dosyayı text() ya da arrayBuffer() ile tek seferde okumak, dosyanın tamamını belleğe almak demektir. Birkaç yüz megabaytlık bir video için bu ciddi bir yük. Sadece bir bölüm lazımsa slice() kullan; hepsini işlemen gerekiyorsa stream() ile parça parça oku.

Typed array'lerde taşma sessizdir. Uint8Array'e 300 yazarsan hata almazsın, 44 kaydedilir. Hesap yaptığın değerleri yazmadan önce aralığı sen kontrol et ya da sınıra yapıştırmak istiyorsan Uint8ClampedArray kullan.

Byte sırasını varsayma. DataView varsayılan olarak big-endian okur, typed array'ler ise makinenin sırasıyla, pratikte little-endian. Bir dosya formatını okurken formatın belgesine bak ve DataView method'larına ikinci parametreyi her zaman açıkça ver; kodu okuyan da neyin kastedildiğini anlar.

Son olarak slice ile subarray arasındaki farkı karıştırma. Typed array'lerde slice() yeni bir buffer'a kopya alır, subarray() ise aynı buffer'a bakan yeni bir gözlük üretir. Birini değiştirince diğerinin de değişmesi seni şaşırtmasın.

Küçük Bir Deneme

★☆☆ Dosya okuma örneğini değiştir: seçilen her .txt dosyası için ilk 200 karakter yerine dosyadaki kelime sayısını ve satır sayısını göster.

★★☆ Çalıştırmadan önce çıktıyı tahmin et, sonra konsolda dene:

const a = new Uint8Array([10, 20, 30, 40]);
const b = a.subarray(1, 3);
const c = a.slice(1, 3);
b[0] = 99;
c[1] = 77;
console.log(a, b, c);

★★★ Sürükle-bırak örneğine bir dosya türü dedektörü ekle: bırakılan her dosyanın ilk 8 byte'ını toHex() ile ekrana yaz ve bu byte'lara bakarak dosyanın PNG (89504e47 ile başlar), JPEG (ffd8ff ile başlar) ya da bilinmeyen bir tür olduğunu söyle. Sonra bir .png dosyasının uzantısını .jpg yapıp dene; file.type ne diyor, senin dedektörün ne diyor?

Artık bir görselin byte'larına kadar inebiliyoruz. Bir sonraki yazıda bu görseli bir canvas'a çizecek, şekiller ve animasyonlarla oynayacak ve piksellerine tek tek dokunacağız.