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.prototypeolur; 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
thishiç 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.
varile bildirilen döngü sayacı tek bir değişkendir;lether 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.