İçeriğe geç
academia.sh

Ders 09 / 17

Kapanışlar

Kapatılan değişkenlerin yaşam süresi ve paylaşımı, kapanışla kapsülleme, aynı türün üç kuruluşunun karşılaştırması, döngü değişkeni tuzağı ve not alma kalıbı.

İçindekiler

Önceki dersin son örneğinde bir sayaç, durumunu nesne özelliğinde değil, kendisini üreten fonksiyonun yerel değişkeninde tuttu. Fonksiyon döndü, ama değişken yaşamaya devam etti ve yalnızca döndürülen fonksiyonlar tarafından görülebildi.

Kapanış (closure), bir fonksiyonun tanımlandığı kapsamdaki değişkenlere, o kapsam sona erdikten sonra da erişebilmesidir. Programlama Temelleri kursundaki Kapsam ve Yaşam Süresi dersi kavramı tanıtmıştı; bu ders onu nesne modelinin içine yerleştirir. Ortaya çıkan sonuç kursun ekseni açısından belirleyicidir: kapanış, durumu prototip zincirine hiç dokunmadan gizleyen ikinci bir kuruluş yolu sunar.

Kapanışla Kapsülleme

Ölçüm kaydını bu kez bir fabrika işleviyle üretelim. Ham değer ve kalibrasyon kayması işlevin parametrelerinde kalır; döndürülen nesneye hiç yazılmaz.

function olcumKaydiOlustur(sensor, hamDeger, kayma) {
  let okumaSayisi = 0;

  function okunanDeger() {
    okumaSayisi += 1;
    return hamDeger + kayma;
  }

  return {
    sensor,
    oku: okunanDeger,
    bicimle() {
      return `${sensor}: ${okunanDeger().toFixed(2)}`;
    },
    denetim() {
      return `okuma=${okumaSayisi}`;
    },
  };
}

const kayit = olcumKaydiOlustur("S-01", 21.4, -0.4);

console.log(kayit.oku().toFixed(2));
console.log(kayit.bicimle());
console.log(kayit.denetim());
console.log(JSON.stringify(kayit));
console.log(Object.keys(kayit).join(","));
console.log(kayit.hamDeger);
console.log(kayit.okumaSayisi);
21.00
S-01: 21.00
okuma=2
{"sensor":"S-01"}
sensor,oku,bicimle,denetim
undefined
undefined

Ham değer, kayma ve okuma sayacı hiçbir listeleme işleminde görünmez; dizileştirmeye girmez; dışarıdan okunamaz ve yazılamaz. Sınıf sözdiziminin özel alanlarıyla aynı sonuçtur, ama mekanizması bambaşkadır: gizlilik özellik tanımlayıcılarından değil, kapsamdan gelir. Bir değişken yalnızca onu gören fonksiyonlar tarafından erişilebilir olduğu için, kapsam sınırının kendisi kapsüllemeyi sağlar.

Denetim sayacının 2 olması, bicimle çağrısının da aynı iç fonksiyonu kullandığını gösterir: üç fonksiyon aynı kapsamı — dolayısıyla aynı değişkenleri — paylaşır.

Aynı Türün Üç Kuruluşu

Kurs boyunca kullanılan ölçüm kaydı artık üç ayrı biçimde kurulabiliyor. Üçünü yan yana koymak, ödünleşimi tek bir çıktıda görünür kılar.

function duzKayit(sensor, deger) {
  return {
    sensor,
    deger,
    bicimle() {
      return `${this.sensor}: ${this.deger}`;
    },
  };
}

function kapanisliKayit(sensor, deger) {
  return {
    bicimle() {
      return `${sensor}: ${deger}`;
    },
  };
}

class SinifKayit {
  constructor(sensor, deger) {
    this.sensor = sensor;
    this.deger = deger;
  }
  bicimle() {
    return `${this.sensor}: ${this.deger}`;
  }
}

const duzA = duzKayit("S-01", 21.4);
const duzB = duzKayit("S-02", 19.8);
const kapA = kapanisliKayit("S-01", 21.4);
const kapB = kapanisliKayit("S-02", 19.8);
const sinA = new SinifKayit("S-01", 21.4);
const sinB = new SinifKayit("S-02", 19.8);

console.log([duzA.bicimle(), kapA.bicimle(), sinA.bicimle()].join(" | "));

console.log(`duz  : paylasim=${duzA.bicimle === duzB.bicimle}, kendi=${Object.keys(duzA).join("+")}, json=${JSON.stringify(duzA)}`);
console.log(`kapan: paylasim=${kapA.bicimle === kapB.bicimle}, kendi=${Object.keys(kapA).join("+")}, json=${JSON.stringify(kapA)}`);
console.log(`sinif: paylasim=${sinA.bicimle === sinB.bicimle}, kendi=${Object.keys(sinA).join("+")}, json=${JSON.stringify(sinA)}`);

console.log(Object.getPrototypeOf(duzA) === Object.prototype);
console.log(Object.getPrototypeOf(kapA) === Object.prototype);
console.log(Object.getPrototypeOf(sinA) === SinifKayit.prototype);

const kopukSinif = sinA.bicimle;
const kopukKapanis = kapA.bicimle;
try {
  kopukSinif();
} catch (hata) {
  console.log(`sinif kopuk: ${hata.constructor.name}`);
}
console.log(`kapanis kopuk: ${kopukKapanis()}`);
S-01: 21.4 | S-01: 21.4 | S-01: 21.4
duz  : paylasim=false, kendi=sensor+deger+bicimle, json={"sensor":"S-01","deger":21.4}
kapan: paylasim=false, kendi=bicimle, json={}
sinif: paylasim=true, kendi=sensor+deger, json={"sensor":"S-01","deger":21.4}
true
true
true
sinif kopuk: TypeError
kapanis kopuk: S-01: 21.4

İlk satır üç kuruluşun aynı davranışı ürettiğini doğrular. Kalan satırlar farkları sıralar:

  • Paylaşım. Yalnızca sınıf kuruluşunda fonksiyon tektir; diğer ikisinde her nesne kendi fonksiyonunu taşır. Bin kayıt için sınıf bir fonksiyon, diğerleri bin fonksiyon üretir.
  • Görünürlük. Kapanışlı kuruluşta veri hiç özellik olmadığı için dizileştirme boş nesne verir. Verinin dışarı çıkması gerekiyorsa açık bir yöntem yazılmalıdır.
  • Zincir. Fabrika işlevleri sıradan nesne değişmezi döndürdüğü için prototipleri Object.prototype olur; tür sorgusu, ortak davranış ekleme ve alt tür türetme yollarının hiçbiri çalışmaz.
  • Bağlanma. Kapanışlı kuruluşta this hiç kullanılmadığı için yöntem nesnesinden koparıldığında bile çalışır; sınıf yöntemi aynı durumda hata verir.

Seçim ölçütü buradan çıkar. Çok sayıda nesne, tür sorgusu, alt tür ve ortak davranış gerekiyorsa sınıf; az sayıda nesne, kesin gizlilik ve bağlamdan bağımsız çağrı gerekiyorsa kapanış. İkisi birbirinin yerine geçen seçenekler değil, farklı maliyet profilleri olan iki kuruluştur.

Kapatılan Değişkenin Yaşam Süresi

Kapanış bir değeri değil, bir değişkeni kapatır. Aynı kapsamı paylaşan fonksiyonlar aynı değişkeni görür; farklı çağrılardan üretilen kapanışlar ise ayrı kapsamlara sahiptir.

function olcumSayaciOlustur(baslangic) {
  let sayi = baslangic;
  return {
    artir: () => (sayi += 1),
    azalt: () => (sayi -= 1),
    oku: () => sayi,
  };
}

const birinci = olcumSayaciOlustur(0);
const ikinci = olcumSayaciOlustur(100);

birinci.artir();
birinci.artir();
ikinci.azalt();

console.log(birinci.oku());
console.log(ikinci.oku());

function tamponOlustur() {
  const degerler = [];
  return {
    ekle(deger) {
      degerler.push(deger);
      return degerler.length;
    },
    ortalama() {
      if (degerler.length === 0) return 0;
      return degerler.reduce((toplam, d) => toplam + d, 0) / degerler.length;
    },
    boyut: () => degerler.length,
  };
}

const tampon = tamponOlustur();
tampon.ekle(21.4);
tampon.ekle(19.8);
tampon.ekle(25.1);

console.log(tampon.boyut());
console.log(tampon.ortalama().toFixed(3));
console.log(Object.keys(tampon).join(","));
2
99
3
22.100
ekle,ortalama,boyut

İki sayaç birbirinden bağımsızdır; her olcumSayaciOlustur çağrısı yeni bir kapsam üretir. Buna karşılık aynı çağrıdan dönen üç fonksiyon tek bir sayi değişkenini paylaşır — biri değiştirdiğinde diğeri değişikliği görür.

Yaşam süresi kuralı da buradan çıkar: kapatılan değişken, onu gören son fonksiyon erişilebilir olduğu sürece yaşar. tamponOlustur işlevi döndükten sonra degerler dizisine hiçbir dış başvuru yoktur, ama üç fonksiyon onu gördüğü için dizi bellekte kalır. Bu, kapanışların bellek maliyetinin kaynağıdır: büyük bir veri yapısını kapatan küçük bir fonksiyonun saklanması, o veri yapısının tamamını yaşatır. Bellek yaşam döngüsü ve erişilebilirlik temelli toplama, Eşzamansız JavaScript ve Çalışma Zamanı kursunun konusudur.

Döngü Değişkeni Tuzağı

Kapanışın değişkeni kapattığı kuralının en bilinen sonucu, döngü içinde üretilen fonksiyonlarda görülür.

const varIsleri = [];
for (var sayac = 0; sayac < 3; sayac += 1) {
  varIsleri.push(() => `sensor-${sayac}`);
}

const letIsleri = [];
for (let sayac = 0; sayac < 3; sayac += 1) {
  letIsleri.push(() => `sensor-${sayac}`);
}

console.log(varIsleri.map((f) => f()).join(","));
console.log(letIsleri.map((f) => f()).join(","));

const elleIsler = [];
for (var i = 0; i < 3; i += 1) {
  elleIsler.push(
    (function (sabitlenen) {
      return () => `sensor-${sabitlenen}`;
    })(i),
  );
}
console.log(elleIsler.map((f) => f()).join(","));
sensor-3,sensor-3,sensor-3
sensor-0,sensor-1,sensor-2
sensor-0,sensor-1,sensor-2

var ile bildirilen sayaç fonksiyon kapsamlıdır: tüm döngü boyunca tek bir değişken vardır ve üç kapanış da onu kapatır. Döngü bittiğinde değişkenin değeri 3 olduğu için üç fonksiyon da aynı sonucu verir. let her yinelemede yeni bir bağlama üretir, bu yüzden her kapanış kendi değişkenini kapatır.

Üçüncü blok, blok kapsamlı bildirimler dile girmeden önce kullanılan çözümü gösterir: her yinelemede hemen çağrılan bir işlevle yeni bir kapsam açmak ve sayacı parametre olarak sabitlemek. Bu kalıp sonraki dersin konusudur.

Durum Taşıyan Sarmalayıcı

Kapanışın en verimli kullanımı, bir fonksiyona onu değiştirmeden durum eklemektir. Programlama Temelleri kursundaki Özyineleme dersinde tanıtılan not alma (memoization) tam olarak budur: hesaplanan sonuçlar kapanıştaki bir sözlükte saklanır.

function notAlarakSar(islev) {
  const onbellek = Object.create(null);
  let gercekCagri = 0;

  function sarilmis(anahtar) {
    if (anahtar in onbellek) return onbellek[anahtar];
    gercekCagri += 1;
    const sonuc = islev(anahtar);
    onbellek[anahtar] = sonuc;
    return sonuc;
  }

  sarilmis.gercekCagriSayisi = () => gercekCagri;
  return sarilmis;
}

const acikTablo = { "S-01": -0.4, "S-02": 0.15, "S-03": 0 };
const guvenliTablo = Object.assign(Object.create(null), acikTablo);

const acikOku = notAlarakSar((sensor) => acikTablo[sensor] ?? 0);
const guvenliOku = notAlarakSar((sensor) => guvenliTablo[sensor] ?? 0);

console.log(acikOku("S-01"));
console.log(acikOku("S-01"));
console.log(acikOku("S-02"));
console.log(acikOku("S-01"));
console.log(acikOku.gercekCagriSayisi());

console.log(typeof acikOku("toString"));
console.log(typeof guvenliOku("toString"));
console.log(guvenliOku("toString"));
console.log(guvenliOku.gercekCagriSayisi());
console.log(typeof acikOku.onbellek);
-0.4
-0.4
0.15
-0.4
2
function
number
0
1
undefined

Dört çağrının yalnızca ikisi asıl işlevi çalıştırır; gerisi önbellekten döner. Sayaç fonksiyona bir özellik olarak iliştirilmiştir, ama sayacın kendisi hâlâ kapanıştadır — son satır onbellek sözlüğünün dışarıdan görünmediğini doğrular.

Altıncı ve yedinci satırlar ilk dersteki uyarının bedelini gösterir. Sıradan bir nesne değişmeziyle kurulan kalibrasyon tablosunda "toString" anahtarı sorulduğunda zincirden gelen fonksiyon bulunur ve ?? işleci onu boş saymaz. Object.create(null) ile kurulan tabloda zincir olmadığı için doğru sonuç döner. Anahtarları program dışından gelen her eşlemede aynı önlem gerekir; dilin bu iş için ayrılmış türü olan Map, İleri Koleksiyonlar konusunda ele alınacaktır.

Özet

  • Kapanış, bir fonksiyonun tanımlandığı kapsamdaki değişkenlere o kapsam sona erdikten sonra da erişebilmesidir.
  • Kapanışla kurulan gizlilik özellik tanımlayıcılarından değil kapsam sınırından gelir; veri hiç özellik olmadığı için listelenmez ve dizileştirilmez.
  • Kapanışlı kuruluş bağlamdan bağımsızdır ama davranışı paylaşmaz ve prototip zinciri kurmaz; sınıf kuruluşu bunun tersidir.
  • Kapanış değeri değil değişkeni kapatır; aynı kapsamdan üretilen fonksiyonlar aynı değişkeni paylaşır, farklı çağrılar bağımsız kapsamlar üretir.
  • Kapatılan değişken, onu gören son fonksiyon erişilebilir olduğu sürece yaşar; bellek maliyeti buradan doğar.
  • var ile bildirilen döngü sayacı tek bir değişkendir; let her yinelemede yeni bağlama üretir.

Sonraki Adım

Döngü tuzağının eski çözümünde bir fonksiyon tanımlanır tanımlanmaz çağrılıyor ve yalnızca yeni bir kapsam açmak için kullanılıyordu. Bu kalıbın adı vardır ve blok kapsamlı bildirimlerden önce kütüphane sınırlarını çizmenin de tek yoluydu. Sonraki ders hemen çağrılan fonksiyonları, kapsam yalıtımı işlevini ve bu işlevin dilin sonraki yeteneklerince ne ölçüde devralındığını ele alacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat