Proxy, bir nesnenin önüne geçen ve o nesne üzerindeki temel işlemleri (özellik okuma, yazma, silme, in kontrolü, fonksiyon çağırma gibi) yakalayıp yeniden tanımlamanızı sağlayan bir yapıdır. new Proxy(target, handler) ile oluşturulur; handler içindeki get, set gibi fonksiyonlara "trap" denir. Reflect ise bu trap'lerle aynı adlı metotlar sunan bir isim alanıdır ve trap içinden varsayılan davranışa dönmenin en temiz yoludur.

Proxy en çok doğrulama, loglama, varsayılan değer verme ve değişiklik izleme için kullanılır. Bu yazıda trap listesini, çalışan doğrulama ve loglama örneklerini, Proxy.revocable kullanımını ve özel alanlar, iç slotlar ile invariant kuralları gibi önemli kısıtları görüyorsunuz.

Temel kullanım: get trap'i

Handler boşsa proxy hedef nesne gibi davranır ve tüm işlemleri ona iletir. Bir trap tanımladığınızda o işlem sizin fonksiyonunuzdan geçer. Aşağıdaki proxy, olmayan özellikler için undefined yerine 0 döndürür:

javascript
const sayac = new Proxy({}, {
  get(hedef, ozellik) {
    return ozellik in hedef ? hedef[ozellik] : 0;
  },
});
sayac.elma = 3;
console.log(sayac.elma, sayac.armut); // 3 0

Trap'ler ve karşılık gelen işlemler

Trap'ler ve karşılık gelen işlemler: obj.x, obj["x"], obj.x = 1, "x" in obj, deleteProperty

Her nesnenin [[Get]], [[Set]] gibi iç metotları vardır ve dildeki her etkileşim sonunda bunlardan birine iner. Proxy bu iç metotların her birine bir trap sunar. Reflect üzerinde de aynı adlı statik metotlar bulunur.

TrapYakaladığı işlemReflect karşılığı
getobj.x, obj["x"]Reflect.get
setobj.x = 1Reflect.set
has"x" in objReflect.has
deletePropertydelete obj.xReflect.deleteProperty
ownKeysObject.keys, Reflect.ownKeysReflect.ownKeys
definePropertyObject.definePropertyReflect.defineProperty
getOwnPropertyDescriptorObject.getOwnPropertyDescriptorReflect.getOwnPropertyDescriptor
getPrototypeOf / setPrototypeOfObject.getPrototypeOf / setPrototypeOfReflect.getPrototypeOf / setPrototypeOf
isExtensible / preventExtensionsObject.isExtensible / preventExtensionsReflect.isExtensible / preventExtensions
applyfonksiyon çağrısı f()Reflect.apply
constructnew F()Reflect.construct

apply ve construct yalnızca hedef bir fonksiyon olduğunda anlamlıdır.

Doğrulama örneği: set trap'i

set trap'i dört argüman alır: hedef, özellik adı, yeni değer ve alıcı (receiver). Atama başarılıysa true döndürmelidir. Kuralı geçen değerleri Reflect.set ile hedefe yazmak, varsayılan davranışı (setter'lar dahil) korur.

javascript
const kural = {
  set(hedef, ozellik, deger, alici) {
    if (ozellik === "yas") {
      if (!Number.isInteger(deger)) throw new TypeError("yaş tam sayı olmalı");
      if (deger < 0 || deger > 150) throw new RangeError("yaş 0 ile 150 arasında olmalı");
    }
    return Reflect.set(hedef, ozellik, deger, alici);
  },
};
const kisi = new Proxy({}, kural);
kisi.yas = 30;
console.log(kisi.yas); // 30
kisi.yas = "otuz"; // TypeError: yaş tam sayı olmalı
kisi.yas = 200; // RangeError: yaş 0 ile 150 arasında olmalı

Loglama örneği: Reflect ile iletme

Loglama örneği: Reflect ile iletme

Aşağıdaki izle fonksiyonu okuma, yazma ve silme işlemlerini loglar, sonra işi Reflect ile hedefe bırakır. Dizi örneği ilginç bir ayrıntıyı gösterir: push gibi metotlar da iç metotlarla çalıştığı için tek bir push çağrısı birden fazla trap tetikler.

javascript
function izle(nesne, ad) {
  return new Proxy(nesne, {
    get(hedef, ozellik, alici) {
      console.log(`[okuma] ${ad}.${String(ozellik)}`);
      return Reflect.get(hedef, ozellik, alici);
    },
    set(hedef, ozellik, deger, alici) {
      console.log(`[yazma] ${ad}.${String(ozellik)} = ${JSON.stringify(deger)}`);
      return Reflect.set(hedef, ozellik, deger, alici);
    },
    deleteProperty(hedef, ozellik) {
      console.log(`[silme] ${ad}.${String(ozellik)}`);
      return Reflect.deleteProperty(hedef, ozellik);
    },
  });
}

const ayar = izle({ tema: "açık" }, "ayar");
ayar.tema = "koyu"; // [yazma] ayar.tema = "koyu"
console.log(ayar.tema); // [okuma] ayar.tema, sonra: koyu
delete ayar.tema; // [silme] ayar.tema

const liste = izle([], "liste");
liste.push("a");
// [okuma] liste.push
// [okuma] liste.length
// [yazma] liste.0 = "a"
// [yazma] liste.length = 1

String(ozellik) kullanmamızın nedeni, özellik adının bir symbol olabilmesidir; symbol'ler şablon metne doğrudan eklenemez. Bkz. Symbol nedir.

has, ownKeys ve apply trap'leri

has trap'i in operatörünü, ownKeys ise anahtar listeleyen işlemleri yakalar. Aşağıdaki örnek alt çizgiyle başlayan alanları bu iki işlemden gizler; ancak doğrudan okumayı engellemez. Gerçek bir gizleme için get trap'ini de yazmanız gerekir.

javascript
const gizli = new Proxy({ ad: "Ali", _sifre: "1234" }, {
  has(hedef, ozellik) {
    if (String(ozellik).startsWith("_")) return false;
    return Reflect.has(hedef, ozellik);
  },
  ownKeys(hedef) {
    return Reflect.ownKeys(hedef).filter((k) => typeof k !== "string" || !k.startsWith("_"));
  },
});
console.log("_sifre" in gizli, "ad" in gizli); // false true
console.log(Object.keys(gizli)); // [ 'ad' ]
console.log(gizli._sifre); // 1234  (get trap'i yok, okuma engellenmedi)
javascript
function topla(a, b) { return a + b; }
const olculu = new Proxy(topla, {
  apply(hedef, thisArg, argumanlar) {
    console.log("çağrı:", argumanlar);
    return Reflect.apply(hedef, thisArg, argumanlar);
  },
});
console.log(olculu(2, 3));
// çağrı: [ 2, 3 ]
// 5

Proxy.revocable: erişimi geri almak

Proxy.revocable(target, handler), proxy ve revoke alanlarına sahip bir nesne döndürür. revoke() çağrıldıktan sonra proxy üzerindeki her işlem TypeError fırlatır; typeof ise hiçbir trap tetiklemediği için çalışmaya devam eder. MDN, bunun hedef nesnenin ömrünü kontrol etmenin bir yolu olduğunu belirtir: proxy iptal edildiğinde hedef ve handler bağlantısı koparılır.

javascript
const { proxy, revoke } = Proxy.revocable({ deger: 1 }, {});
console.log(proxy.deger); // 1
revoke();
proxy.deger;
// TypeError: Cannot perform 'get' on a proxy that has been revoked
console.log(typeof proxy); // object

Kısıtlar ve tuzaklar

Kısıtlar ve tuzaklar: Proxy, hedefle aynı nesne değildir, Özel alanlar ve iç slotlar iletilmez, Invariant kuralları, set trap'i false döndürürse

Proxy, hedefle aynı nesne değildir

new Proxy(hedef, {}) === hedef ifadesi false döner. Kimlik karşılaştırması yapan kod (örneğin bir Set veya WeakMap anahtarı) proxy ile orijinali farklı görür.

Özel alanlar ve iç slotlar iletilmez

Boş handler'lı bir proxy düz nesnelerde sorunsuz çalışır, ama #özel alanları olan sınıf örneklerinde ve Map gibi iç slot kullanan yerleşik nesnelerde çalışmaz. Getter çalışırken this proxy'nin kendisi olduğu için özel alana erişilemez. Çözüm, değeri doğrudan hedeften okumaktır.

javascript
class Kasa {
  #bakiye = 100;
  get bakiye() { return this.#bakiye; }
}
new Proxy(new Kasa(), {}).bakiye;
// TypeError: Cannot read private member #bakiye from an object whose class did not declare it

console.log(new Proxy(new Kasa(), { get(h, o) { return h[o]; } }).bakiye); // 100

new Proxy(new Map(), {}).size;
// TypeError: Method get Map.prototype.size called on incompatible receiver #<Map>

Metotlar için de aynı sorun vardır; MDN, dönen fonksiyonun this değerini hedefe yönlendiren bir sarmalayıcı örneği verir.

Invariant kuralları

Proxy her şeyi değiştiremez. Hedefte yazılamaz ve yapılandırılamaz (non-writable, non-configurable) bir özellik varsa get trap'i onun gerçek değerini döndürmek zorundadır. Bu kuralları çiğneyen trap'ler TypeError fırlatır:

javascript
const sabit = {};
Object.defineProperty(sabit, "pi", { value: 3.14, writable: false, configurable: false });
new Proxy(sabit, { get() { return 3; } }).pi;
// TypeError: 'get' on proxy: property 'pi' is a read-only and non-configurable data property
// on the proxy target but the proxy did not return its actual value (expected '3.14' but got '3')

set trap'i false döndürürse

set trap'i false döndürdüğünde strict mode'daki atama TypeError fırlatır, sloppy mode'da ise sessizce yok sayılır. Reflect.set ise hata yerine false döndürür. Modüller otomatik strict olduğu için bu fark özellikle önemlidir; ayrıntı strict mode yazısında.

javascript
// dosya: ornek.mjs (strict)
const salt = new Proxy({}, { set() { return false; } });
salt.x = 1;
// TypeError: 'set' on proxy: trap returned falsish for property 'x'
console.log(Reflect.set(salt, "x", 1)); // false

Trap içinde proxy'yi tekrar kullanmak

Trap içinde receiver (yani proxy) üzerinden aynı işlemi yapmak trap'i yeniden tetikler ve sonsuz döngüye girer. Hedefe Reflect ile ya da doğrudan hedef üzerinden erişin.

javascript
const yanlis = new Proxy({}, {
  get(hedef, ozellik, alici) { return alici[ozellik]; },
});
yanlis.a;
// RangeError: Maximum call stack size exceeded

Pratik senaryo: salt okunur görünüm

Bir yapılandırma nesnesini uygulamanın geri kalanına verirken değiştirilmesini istemiyorsanız yazma, silme ve özellik tanımlama trap'lerini aynı fonksiyona bağlayabilirsiniz. Object.freeze da benzer bir sonuç verir; fark, proxy'nin her denemede kendi mesajınızla hata fırlatması ve orijinal nesneyi dondurmamasıdır. Orijinal nesneye referansı olan kod onu değiştirmeye devam edebilir, proxy ise yalnızca kendi üzerinden yapılan işlemleri engeller.

javascript
function saltOkunur(nesne) {
  const engelle = () => { throw new TypeError("bu nesne salt okunur"); };
  return new Proxy(nesne, { set: engelle, deleteProperty: engelle, defineProperty: engelle });
}
const ayar = saltOkunur({ port: 3000 });
console.log(ayar.port); // 3000
ayar.port = 80; // TypeError: bu nesne salt okunur
delete ayar.port; // TypeError: bu nesne salt okunur
Object.defineProperty(ayar, "yeni", { value: 1 }); // TypeError: bu nesne salt okunur

Nesnelerin dondurulması ve özellik tanımlayıcıları hakkında daha fazla bilgi için Object metotları yazısına bakabilirsiniz. Proxy'yi her sorun için ilk araç olarak görmeyin: davranışı gizli ve dolaylı hale getirdiği için kodu okuyan kişinin trap'leri bilmesi gerekir. Basit bir doğrulama için setter ya da açık bir fonksiyon çoğu zaman daha anlaşılırdır.

Reflect neden kullanılır?

Reflect bir kurucu değildir; Math gibi yalnızca statik metotlar içerir. Ana kullanım alanı trap içinde varsayılan davranışa dönmektir. Bunun yanında başarılı olup olmadığını boolean ile bildiren metotlar da sunar: Object.defineProperty başarısızlıkta hata fırlatırken Reflect.defineProperty false döndürür, böylece try...catch yerine if kullanabilirsiniz. Reflect.has, in operatörünün fonksiyon halidir.

Sık Sorulan Sorular

Proxy performansı etkiler mi?

Her işlem bir fonksiyon çağrısından geçtiği için ek iş yapılır. Yerel kaynaklarda bunun için bir ölçüm yoktur; sık çalışan kodda kullanıyorsanız kendi senaryonuzda ölçmeniz gerekir.

Proxy ile nesnedeki tüm değişiklikleri izleyebilir miyim?

Yalnızca proxy üzerinden yapılan işlemleri. Hedef nesneye doğrudan referansı olan kod, proxy'yi atlayarak değişiklik yapabilir. İç içe nesneleri izlemek için get trap'inde dönen nesneleri de proxy ile sarmanız gerekir.

Reflect olmadan trap yazılabilir mi?

Evet, hedef[ozellik] = deger gibi doğrudan işlemler de çalışır. Reflect ise receiver argümanını doğru iletir, getter ve setter'larda this değerini korur ve her iç metot için ayrı sözdizimi hatırlamanızı gerektirmez.

Proxy hangi nesnelerle kullanılabilir?

Hedef herhangi bir nesne olabilir: düz nesne, dizi, fonksiyon, hatta başka bir proxy. İç slot kullanan yerleşik nesnelerde ve özel alanlı sınıflarda yukarıdaki kısıtları hesaba katın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Object metotları · Symbol nedir · Class ve özel alanlar · Strict mode