18 Ekim 2025

Proxy ve Reflect

Bir proxy (vekil), başka bir nesneyi sarmalar ve özellik okuma/yazma gibi işlemleri engeller (intercept eder). Bu işlemleri kendisi yönetebilir veya şeffaf bir şekilde hedef nesnenin yönetmesine izin verebilir.

Proxy’ler birçok kütüphanede ve bazı tarayıcı framework’lerinde kullanılır. Bu bölümde birçok pratik kullanım örneği göreceğiz.

Sözdizimi:

let proxy = new Proxy(target, handler)
  • target – sarmalanacak nesne; fonksiyonlar da dahil olmak üzere herhangi bir şey olabilir.
  • handler – işlemleri yakalayan (“trap” adı verilen) metotları içeren bir nesnedir. Örneğin, bir özelliği okumak için get, bir özelliğe yazmak için set gibi.

proxy üzerinde bir işlem yapılırsa, handler içinde o işleme karşılık gelen bir trap varsa, o çalıştırılır; yoksa işlem targetüzerinde gerçekleştirilir.

Basit bir örnek olarak, hiç trap içermeyen bir proxy oluşturalım:

let target = {};
let proxy = new Proxy(target, {}); // boş handler

proxy.test = 5; // proxy’ye yazma (1)
alert(target.test); // 5, özellik target üzerinde belirdi!

alert(proxy.test); // 5, proxy’den de okuyabiliyoruz (2)

for(let key in proxy) alert(key); // test, döngü çalışıyor (3)

Hiç trap olmadığından, proxy üzerindeki tüm işlemler target’a yönlendirilir.

  1. proxy.test= yazma işlemi, değeri target üzerine yazar.
  2. proxy.test okuma işlemi, değeri target’tan döndürür.
  3. proxy üzerinde döngü yapmak, target’taki değerleri döndürür.

Gördüğümüz gibi, trap olmadan proxy, target üzerinde şeffaf bir sarmalayıcı gibi davranır.

Proxy, özel bir “egzotik nesnedir”. Kendi özellikleri yoktur. Boş bir handler ile, işlemleri tamamen target’a yönlendirir.

Eğer sihirli bir davranış istiyorsak, trap’ler eklememiz gerekir.

Proxy spesifikasyonu’nda tanımlanmış bir dizi dahili nesne işlemi vardır. Bir proxy, bunlardan herhangi birini yakalayabilir; bunun için handler’a karşılık gelen metodu eklememiz yeterlidir.

Aşağıdaki tabloda:

  • İçsel Metot(Internal Method) Spesifikasyondaki dahili işlemin adıdır. Örneğin, [[Get]] bir özelliği okuma işlemidir.
  • Handler Metodu(Handler Method) handler’a eklememiz gereken metot adıdır; bu metot işlemi yakalar ve özel bir davranış tanımlar.
İçsel Metot Handler Metodu Yakalanan İşlem(Traps)…
[[Get]] get özelliği okuma
[[Set]] set özelliğe yazma
[[HasProperty]] has in operatörü
[[Delete]] deleteProperty delete operatörü
[[Call]] apply fonksiyon çağrısı
[[Construct]] construct new operatörü
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf
[[IsExtensible]] isExtensible Object.isExtensible
[[PreventExtensions]] preventExtensions Object.preventExtensions
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor
[[DefineOwnProperty]] defineProperty Object.defineProperty, Object.defineProperties
[[OwnPropertyKeys]] ownKeys Object.keys, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, yineleme anahtarları
Değişmez kurallar (Invariants)

JavaScript, bazı değişmez kuralları zorunlu kılar. Bu kurallar, içsel metotlar ve trap’lerin belirli koşulları yerine getirmesini sağlar.

Çoğu, dönüş değerleriyle ilgilidir:

  • [[Set]] başarılıysa true, değilse false döndürmelidir.
  • [[Delete]] başarılıysa true, değilse false döndürmelidir.
  • …ve benzerleri; aşağıda örneklerde göreceğiz.

Bazı diğer kurallar:

  • [[GetPrototypeOf]] çağrıldığında, proxy nesnesinin prototipi, hedef nesneninkiyle aynı olmalıdır.

Yani, bir proxy’nin prototipini okuduğumuzda, her zaman hedef nesnenin prototipini döndürmelidir. getPrototypeOf trap bu işlemi yakalayabilir ama bu kurala uymalıdır.

Bu değişmez kurallar, dilin tutarlı ve doğru çalışmasını sağlar. Tüm liste spesifikasyonda bulunur; genellikle sıradışı bir şey yapmadığınız sürece ihlal etmezsiniz.

Haydi bunun pratik örneklerde nasıl çalıştığına bakalım.

“get” tuzağı ile varsayılan değer

En yaygın trap’ler özellik okuma/yazma içindir.

Okumayı yakalamak için, handler içinde get(target, property, receiver) adlı bir metot bulunmalıdır.

Bir özellik okunduğunda tetiklenir:

  • target – hedef nesnedir; new Proxy’ye ilk argüman olarak verilen nesne,
  • property – özellik adı,
  • receiver – eğer özellik bir getter ise, o kodda this olarak kullanılacak nesnedir. Genellikle bu proxy nesnesinin kendisidir (veya proxy’den miras alıyorsak ondan türeyen nesne).

Bir nesne için varsayılan değerleri uygulamak üzere get’i kullanalım.

Örneğin, sayısal bir dizinin var olmayan indeksler için undefined yerine 0 döndürmesini istiyoruz.

Bunu, okumayı yakalayan ve öyle bir özellik yoksa varsayılan değer döndüren bir proxy ile saralım:

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; // varsayılan değer
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (böyle bir değer yok)

Bu yaklaşım geneldir. “Varsayılan” değer mantığını Proxy ile istediğimiz gibi kurabiliriz.

Diyelim ki elimizde ifadeler ve onların çevirilerinden oluşan bir sözlük var:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined

Şu anda, bir ifade yoksa dictionary’den okumak undefined döndürüyor. Ama pratikte, çevrilmemiş bir ifadeyi olduğu gibi bırakmak çoğu zaman undefined’dan daha iyidir. O hâlde, undefined yerine varsayılan değerin çevrilmemiş ifadenin kendisi olmasını sağlayalım.

Bunu başarmak için, okumayı yakalayan bir proxy iledictionary’yi saracağız:

let dictionary = {
  'Hello': 'Hola',
  'Bye': 'Adiós'
};

dictionary = new Proxy(dictionary, {
  get(target, phrase) { // dictionary’den bir özellik okunmasını yakala
    if (phrase in target) { // sözlükte varsa
      return target[phrase]; // çeviriyi döndür
    } else {
      // yoksa, çevrilmemiş ifadeyi döndür
      return phrase;
    }
  }
});

// Sözlükte rastgele ifadeleri ara!
// En kötü ihtimalle çevrilmemiş olarak dönerler.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (çeviri yok)
Proxy, her yerde target yerine kullanılmalıdır

Proxy’nin değişkenin üzerine nasıl yazdığına dikkat edin:

dictionary = new Proxy(dictionary, ...);
numbers = new Proxy(numbers, ...);

Proxy, hedef nesnenin yerini her yerde tamamen almalıdır. Bir nesne proxylenmişse, sonrasında kimse hedef nesneye doğrudan referans vermemelidir. Aksi takdirde işler kolayca karışır.

“set” tuzağı ile doğrulama (Validation)

Şimdi yazma işlemlerini de yakalayalım.

Diyelim ki yalnızca sayılardan oluşan bir dizi istiyoruz. Eğer farklı türde bir değer eklenirse, bir hata fırlatılmalı.

set tuzağı (trap), bir özellik yazıldığında tetiklenir: set(target, property, value, receiver)

  • target – hedef nesnedir; new Proxy’ye ilk argüman olarak verilen nesne,
  • property – özellik adı,
  • value – özellik değeri,
  • receiverget tuzağındakiyle aynıdır; yalnızca özellik bir setter ise önemlidir.

set tuzağı, işlem başarılıysa true, başarısızsa false döndürmelidir (aksi hâlde TypeError oluşur).

Yeni değerleri doğrulamak için bunu kullanalım:

let numbers = [];

numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // özelliğe yazmayı yakala
    if (typeof val == 'number') {
      target[prop] = val;
      return true;
    } else {
      return false;
    }
  }
});

numbers.push(1);
numbers.push(2);
alert("Length is: " + numbers.length); // 2

numbers.push("test"); // TypeError ('set' on proxy returned false)

alert("Bu satıra asla ulaşılmaz (yukarıdaki satırda hata var)");

Dikkat ederseniz, dizinin yerleşik işlevleri hâlâ çalışıyor! Yeni değerler eklendiğinde length özelliği otomatik olarak artıyor. Proxy’miz hiçbir şeyi bozmadı.

Ayrıcapush, unshift gibi değer ekleyen metotları da yeniden tanımlamamız gerekmedi. Çünkü bunlar dahili olarak [[Set]] işlemini kullanırlar ve bu işlem proxy tarafından yakalanır.

Kod bu sayede hem temiz hem de kısa olur.

true döndürmeyi unutmayın

Yukarıda belirtildiği gibi, bazı değişmez kurallar (invariant) vardır.

set işlemi başarılıysa true döndürmelidir.

Eğer yanlış (falsy) bir değer döndürülürse (veya hiç değer döndürülmezse), bu TypeError hatasına neden olur.

“deleteProperty” ve “ownKeys” ile korunan özellikler

Yaygın bir konvansiyona göre, _ (alt çizgi) ile başlayan özellikler ve metotlar içseldir.
Bu tür özelliklere nesnenin dışından erişilmemelidir.

Teknik olarak erişmek mümkündür:

let user = {
  name: "John",
  _password: "secret"
};

alert(user._password); // secret

Şimdi _ ile başlayan özelliklere erişimi engellemek için proxy kullanalım.

Bunun için şu tuzaklara ihtiyacımız var:

  • get okuma sırasında hata fırlatmak için,
  • set yazma sırasında hata fırlatmak için,
  • deleteProperty silme sırasında hata fırlatmak için,
  • ownKeys _ ile başlayan özellikleri for...in döngüsü veya Object.keys() gibi işlemlerden gizlemek için.

Kod şu şekilde olur:

let user = {
  name: "John",
  _password: "***"
};

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error("Erişim reddedildi");
    }
    let value = target[prop];
    return (typeof value === 'function') ? value.bind(target) : value; // (*)
  },
  set(target, prop, val) { // yazma işlemini yakala
    if (prop.startsWith('_')) {
      throw new Error("Erişim reddedildi");
    } else {
      target[prop] = val;
    }
  },
  deleteProperty(target, prop) { // silme işlemini yakala
    if (prop.startsWith('_')) {
      throw new Error("Erişim reddedildi");
    } else {
      delete target[prop];
      return true;
    }
  },
  ownKeys(target) { // özellik listesini yakala
    return Object.keys(target).filter(key => !key.startsWith('_'));
  }
});

// "get" -> _password okunamaz
try {
  alert(user._password); // Hata: Erişim reddedildi
} catch(e) { alert(e.message); }

// "set" -> _password yazılamaz
try {
  user._password = "test"; // Hata: Erişim reddedildi
} catch(e) { alert(e.message); }

// "deleteProperty" -> _password silinemez
try {
  delete user._password; // Hata: Erişim reddedildi
} catch(e) { alert(e.message); }

// "ownKeys" -> _password filtrelenir
for(let key in user) alert(key); // name

Lütfen get tuzağındaki, (*) satırındaki önemli detaya dikkat edin:

get(target, prop) {
  // ...
  let value = target[prop];
  return (typeof value === 'function') ? value.bind(target) : value; // (*)
}

Bir nesne metodu çağrıldığında, örneğin user.checkPassword(), bu metodun _password’a erişebilmesi gerekir:

user = {
  // ...
  checkPassword(value) {
    // nesne metodu _password'ı okuyabilmeli
    return value === this._password;
  }
}

Normalde, user.checkPassword() çağrısında this olarak proxylanmış user geçer (noktadan önceki nesne this olur). Bu yüzden metod this._password’a erişmeye çalıştığında, özellik koruması devreye girer ve hata fırlatır. İşte bu nedenle (*) satırında metodu target’a bağlarız (bind). Böylece o fonksiyon içindeki tüm işlemler doğrudan orijinal nesneye yapılır ve özellik korumasına takılmaz.

Bu çözüm ideal değildir; çünkü metot, proxylanmamış nesneyi başka bir yere aktarabilir ve sonrasında işler karışabilir: Orijinal nesne nerede, proxy nerede?

Bir nesne birden fazla kez proxylanabilir (farklı proxy’ler nesneye farklı “ince ayarlar” ekleyebilir), bu da garip hatalara yol açabilir.

Dolayısıyla, metotları olan karmaşık nesneler için bu tür bir proxy kullanımı önerilmez.

Bir sınıfın özel (private) özellikleri

Modern JavaScript motorları, # ile başlayan sınıf içi özel özellikleri yerel olarak destekler. Bunlar Private and protected properties and methods bölümünde anlatılmıştır. Proxy gerekmez.

Ancak bu özelliklerin de kendine özgü sorunları vardır. Özellikle, kalıtılmazlar.

“has” tuzağı ile “in range” (aralıkta mı?) denetimi

Bir aralık (range) nesnemiz olduğunu varsayalım:

let range = {
  start: 1,
  end: 10
};

Bir sayının range içinde olup olmadığını denetlemek için “in” operatörünü kullanmak istiyoruz.

“has” tuzağı, “in” çağrılarını yakalar: has(target, property)

  • targetnew Proxy’ye ilk argüman olarak geçirilen hedef nesne,
  • property – özellik adı

Örnek:

let range = {
  start: 1,
  end: 10
};

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
});

alert(5 in range); // true
alert(50 in range); // false

Güzel bir sözdizim şekeri, değil mi?

Fonksiyonları sarmalamak: “apply”

Bir fonksiyonun etrafına da proxy sarabiliriz.

apply(target, thisArg, args) tuzağı, proxy’nin fonksiyon gibi çağrılmasını yakalar: