Türkçe

JavaScript Proxy API'sinde uzmanlaşmak isteyen global geliştiriciler için kapsamlı bir rehber. Pratik örnekler, kullanım senaryoları ve performans ipuçlarıyla nesne operasyonlarını yakalayıp özelleştirmeyi öğrenin.

JavaScript Proxy API: Nesne Davranışı Değişikliğine Derinlemesine Bir Bakış

Modern JavaScript'in sürekli gelişen dünyasında, geliştiriciler veriyi yönetmek ve veriyle etkileşim kurmak için sürekli olarak daha güçlü ve zarif yollar ararlar. Sınıflar, modüller ve async/await gibi özellikler kod yazma şeklimizde devrim yaratmış olsa da, ECMAScript 2015'te (ES6) tanıtılan ve genellikle yeterince kullanılmayan güçlü bir metaprogramlama özelliği vardır: Proxy API.

Metaprogramlama kulağa korkutucu gelebilir, ancak basitçe diğer kodlar üzerinde çalışan kod yazma kavramıdır. Proxy API, JavaScript'in bu konudaki birincil aracıdır ve başka bir nesne için bir 'proxy' (vekil) oluşturmanıza olanak tanır. Bu proxy, o nesne için temel işlemleri yakalayabilir ve yeniden tanımlayabilir. Bu, bir nesnenin önüne özelleştirilebilir bir kapı bekçisi koymak gibidir ve nesneye nasıl erişildiği ve değiştirildiği üzerinde size tam kontrol sağlar.

Bu kapsamlı rehber, Proxy API'nin gizemini çözecektir. Temel kavramlarını keşfedecek, çeşitli yeteneklerini pratik örneklerle açıklayacak ve gelişmiş kullanım senaryoları ile performans konularını ele alacağız. Sonunda, Proxy'lerin neden modern framework'lerin temel taşı olduğunu ve daha temiz, daha güçlü ve daha sürdürülebilir kod yazmak için onları nasıl kullanabileceğinizi anlayacaksınız.

Temel Kavramları Anlamak: Target, Handler ve Traps

Proxy API, üç temel bileşen üzerine kuruludur. Bu bileşenlerin rollerini anlamak, proxy'lerde uzmanlaşmanın anahtarıdır.

Bir proxy oluşturma sözdizimi oldukça basittir:

const proxy = new Proxy(target, handler);

Çok temel bir örneğe bakalım. Boş bir handler kullanarak tüm işlemleri hedef nesneye basitçe ileten bir proxy oluşturacağız.


// Orijinal nesne
const target = {
  message: "Merhaba, Dünya!"
};

// Boş bir işleyici. Tüm işlemler hedefe yönlendirilecektir.
const handler = {};

// Proxy nesnesi
const proxy = new Proxy(target, handler);

// Proxy üzerindeki bir özelliğe erişim
console.log(proxy.message); // Çıktı: Merhaba, Dünya!

// İşlem hedefe yönlendirildi
console.log(target.message); // Çıktı: Merhaba, Dünya!

// Proxy aracılığıyla bir özelliği değiştirme
proxy.anotherMessage = "Merhaba, Proxy!";

console.log(proxy.anotherMessage); // Çıktı: Merhaba, Proxy!
console.log(target.anotherMessage); // Çıktı: Merhaba, Proxy!

Bu örnekte, proxy tam olarak orijinal nesne gibi davranır. Asıl güç, handler içinde trap'ler tanımlamaya başladığımızda ortaya çıkar.

Bir Proxy'nin Anatomisi: Yaygın Yakalayıcıları (Traps) Keşfetmek

Handler nesnesi, her biri JavaScript nesnelerinin temel bir iç metoduna karşılık gelen 13'e kadar farklı trap içerebilir. Şimdi en yaygın ve kullanışlı olanları keşfedelim.

Özellik Erişimi Yakalayıcıları

1. `get(target, property, receiver)`

Bu, şüphesiz en çok kullanılan trap'tir. Proxy'nin bir özelliği okunduğunda tetiklenir.

Örnek: Mevcut olmayan özellikler için varsayılan değerler.


const user = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

const userHandler = {
  get(target, property) {
    // Eğer özellik hedefte mevcutsa, onu döndür.
    // Aksi takdirde, varsayılan bir mesaj döndür.
    return property in target ? target[property] : `'${property}' özelliği mevcut değil.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Çıktı: John
console.log(userProxy.age);       // Çıktı: 30
console.log(userProxy.country);   // Çıktı: 'country' özelliği mevcut değil.

2. `set(target, property, value, receiver)`

set trap'i, proxy'nin bir özelliğine değer atandığında çağrılır. Doğrulama, loglama veya salt okunur nesneler oluşturmak için mükemmeldir.

Örnek: Veri doğrulama.


const person = {
  name: 'Jane Doe',
  age: 25
};

const validationHandler = {
  set(target, property, value) {
    if (property === 'age') {
      if (typeof value !== 'number' || !Number.isInteger(value)) {
        throw new TypeError('Yaş bir tam sayı olmalıdır.');
      }
      if (value <= 0) {
        throw new RangeError('Yaş pozitif bir sayı olmalıdır.');
      }
    }

    // Doğrulama geçerse, değeri hedef nesne üzerinde ayarla.
    target[property] = value;

    // Başarıyı belirt.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Bu geçerli
console.log(personProxy.age); // Çıktı: 30

try {
  personProxy.age = 'thirty'; // TypeError fırlatır
} catch (e) {
  console.error(e.message); // Çıktı: Yaş bir tam sayı olmalıdır.
}

try {
  personProxy.age = -5; // RangeError fırlatır
} catch (e) {
  console.error(e.message); // Çıktı: Yaş pozitif bir sayı olmalıdır.
}

3. `has(target, property)`

Bu trap, in operatörünü yakalar. Bir nesnede hangi özelliklerin var görüneceğini kontrol etmenize olanak tanır.

Örnek: 'Özel' özellikleri gizleme.

JavaScript'te yaygın bir gelenek, özel özelliklerin başına alt çizgi (_) koymaktır. has trap'ini kullanarak bunları in operatöründen gizleyebiliriz.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Yokmuş gibi davran
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Çıktı: true
console.log('_apiKey' in dataProxy);   // Çıktı: false (hedefte olmasına rağmen)
console.log('id' in dataProxy);        // Çıktı: true

Not: Bu yalnızca in operatörünü etkiler. Karşılık gelen bir get trap'i de uygulamadığınız sürece dataProxy._apiKey gibi doğrudan erişim hala çalışır.

4. `deleteProperty(target, property)`

Bu trap, bir özellik delete operatörü kullanılarak silindiğinde çalıştırılır. Önemli özelliklerin silinmesini önlemek için kullanışlıdır.

Trap, başarılı bir silme için true, başarısız bir silme için false döndürmelidir.

Örnek: Özelliklerin silinmesini önleme.


const immutableConfig = {
  databaseUrl: 'prod.db.server',
  port: 8080
};

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Korunan özellik silinmeye çalışıldı: '${property}'. İşlem reddedildi.`);
      return false;
    }
    return true; // Özellik zaten mevcut değildi
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Konsol çıktısı: Korunan özellik silinmeye çalışıldı: 'port'. İşlem reddedildi.

console.log(configProxy.port); // Çıktı: 8080 (Silinmedi)

Nesne Numaralandırma ve Tanımlama Yakalayıcıları

5. `ownKeys(target)`

Bu trap, Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() ve Reflect.ownKeys() gibi bir nesnenin kendi özelliklerinin listesini alan işlemler tarafından tetiklenir.

Örnek: Anahtarları filtreleme.

Önceki 'özel' özellik örneğimizle bunu birleştirerek onları tamamen gizleyelim.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const keyHidingHandler = {
  has(target, property) {
    return !property.startsWith('_') && property in target;
  },
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
  },
  get(target, property, receiver) {
    // Doğrudan erişimi de engelle
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

console.log(Object.keys(fullProxy)); // Çıktı: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Çıktı: true
console.log('_apiKey' in fullProxy);   // Çıktı: false
console.log(fullProxy._apiKey);      // Çıktı: undefined

Burada Reflect kullandığımıza dikkat edin. Reflect nesnesi, yakalanabilir JavaScript işlemleri için metotlar sağlar ve bu metotlar, proxy yakalayıcılarıyla aynı adlara ve imzalara sahiptir. Varsayılan davranışın doğru bir şekilde korunmasını sağlamak için orijinal işlemi hedefe yönlendirmek amacıyla Reflect kullanmak en iyi uygulamadır.

Fonksiyon ve Kurucu (Constructor) Yakalayıcıları

Proxy'ler sadece düz nesnelerle sınırlı değildir. Hedef bir fonksiyon olduğunda, çağrıları ve yapılandırmaları yakalayabilirsiniz.

6. `apply(target, thisArg, argumentsList)`

Bu trap, bir fonksiyonun proxy'si çalıştırıldığında çağrılır. Fonksiyon çağrısını yakalar.

Örnek: Fonksiyon çağrılarını ve argümanlarını loglama.


function sum(a, b) {
  return a + b;
}

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`'${target.name}' fonksiyonu şu argümanlarla çağrılıyor: ${argumentsList}`);
    // Orijinal fonksiyonu doğru bağlam ve argümanlarla çalıştır
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`'${target.name}' fonksiyonu şunu döndürdü: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Konsol çıktısı:
// 'sum' fonksiyonu şu argümanlarla çağrılıyor: 5,10
// 'sum' fonksiyonu şunu döndürdü: 15

7. `construct(target, argumentsList, newTarget)`

Bu trap, bir sınıfın veya fonksiyonun proxy'sinde new operatörünün kullanımını yakalar.

Örnek: Singleton deseni uygulaması.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`${this.url} adresine bağlanılıyor...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Yeni örnek oluşturuluyor.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Mevcut örnek döndürülüyor.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Konsol çıktısı:
// Yeni örnek oluşturuluyor.
// db://primary adresine bağlanılıyor...
// Mevcut örnek döndürülüyor.

const conn2 = new ProxiedConnection('db://secondary'); // URL göz ardı edilecek
// Konsol çıktısı:
// Mevcut örnek döndürülüyor.

console.log(conn1 === conn2); // Çıktı: true
console.log(conn1.url); // Çıktı: db://primary
console.log(conn2.url); // Çıktı: db://primary

Pratik Kullanım Senaryoları ve Gelişmiş Desenler

Artık bireysel trap'leri ele aldığımıza göre, gerçek dünya problemlerini çözmek için nasıl birleştirilebileceklerini görelim.

1. API Soyutlama ve Veri Dönüşümü

API'ler genellikle veriyi uygulamanızın kurallarıyla eşleşmeyen bir formatta döndürür (örneğin, snake_case vs. camelCase). Bir proxy bu dönüşümü şeffaf bir şekilde halledebilir.


function snakeToCamel(s) {
  return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}

// Bunun bir API'den gelen ham verimiz olduğunu hayal edin
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // camelCase versiyonunun doğrudan var olup olmadığını kontrol et
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Orijinal özellik adına geri dön
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Artık özellikleri snake_case olarak saklanmalarına rağmen camelCase kullanarak erişebiliriz
console.log(userModel.userId);        // Çıktı: 123
console.log(userModel.firstName);     // Çıktı: Alice
console.log(userModel.accountStatus); // Çıktı: active

2. Gözlemlenebilirler (Observables) ve Veri Bağlama (Modern Framework'lerin Çekirdeği)

Proxy'ler, Vue 3 gibi modern framework'lerdeki reaktivite sistemlerinin arkasındaki motordur. Proxy'lenmiş bir state nesnesindeki bir özelliği değiştirdiğinizde, set trap'i kullanıcı arayüzündeki veya uygulamanın diğer bölümlerindeki güncellemeleri tetiklemek için kullanılabilir.

İşte oldukça basitleştirilmiş bir örnek:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Değişiklik olduğunda geri çağrımı tetikle
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = {
  count: 0,
  message: 'Merhaba'
};

function render(prop, value) {
  console.log(`DEĞİŞİKLİK TESPİT EDİLDİ: '${prop}' özelliği '${value}' olarak ayarlandı. UI yeniden oluşturuluyor...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Konsol çıktısı: DEĞİŞİKLİK TESPİT EDİLDİ: 'count' özelliği '1' olarak ayarlandı. UI yeniden oluşturuluyor...

observableState.message = 'Güle güle';
// Konsol çıktısı: DEĞİŞİKLİK TESPİT EDİLDİ: 'message' özelliği 'Güle güle' olarak ayarlandı. UI yeniden oluşturuluyor...

3. Negatif Dizi İndeksleri

Klasik ve eğlenceli bir örnek, Python gibi dillerde olduğu gibi, -1'in son elemanı ifade ettiği negatif indeksleri desteklemek için yerel dizi davranışını genişletmektir.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Negatif indeksi sondan pozitif bir indekse dönüştür
        property = String(target.length + index);
      }
      return Reflect.get(target, property);
    }
  };
  return new Proxy(arr, handler);
}

const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);

console.log(proxiedArray[0]);  // Çıktı: a
console.log(proxiedArray[-1]); // Çıktı: e
console.log(proxiedArray[-2]); // Çıktı: d
console.log(proxiedArray.length); // Çıktı: 5

Performans Değerlendirmeleri ve En İyi Uygulamalar

Proxy'ler inanılmaz derecede güçlü olsa da, sihirli bir değnek değildirler. Etkilerini anlamak çok önemlidir.

Performans Yükü

Bir proxy, bir dolaylılık katmanı ekler. Proxy'lenmiş bir nesne üzerindeki her işlem, handler'dan geçmelidir, bu da düz bir nesne üzerindeki doğrudan bir işleme kıyasla küçük bir miktar ek yük getirir. Çoğu uygulama için (veri doğrulama veya framework düzeyinde reaktivite gibi), bu ek yük ihmal edilebilir düzeydedir. Ancak, milyonlarca öğeyi işleyen sıkı bir döngü gibi performansı kritik olan kodlarda bu bir darboğaz haline gelebilir. Performans birincil endişe ise her zaman benchmark yapın.

Proxy Değişmezleri (Invariants)

Bir trap, hedef nesnenin doğası hakkında tamamen yalan söyleyemez. JavaScript, proxy trap'lerinin uyması gereken 'değişmezler' (invariants) adı verilen bir dizi kural uygular. Bir değişmezi ihlal etmek TypeError ile sonuçlanır.

Örneğin, deleteProperty trap'i için bir değişmez, hedef nesnedeki karşılık gelen özellik yapılandırılamaz (non-configurable) ise true (başarıyı gösterir) döndürememesidir. Bu, proxy'nin silinemeyen bir özelliği sildiğini iddia etmesini önler.


const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });

const handler = {
  deleteProperty(target, prop) {
    // Bu, değişmezi ihlal edecektir
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Bu bir hata fırlatacaktır
} catch (e) {
  console.error(e.message);
  // Çıktı: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Proxy'leri Ne Zaman Kullanmalı (ve Ne Zaman Kullanmamalı)

Geri Alınabilir (Revocable) Proxy'ler

Bir proxy'yi 'kapatmanız' gerekebilecek senaryolar için (örneğin, güvenlik nedenleriyle veya bellek yönetimi için), JavaScript Proxy.revocable() sağlar. Bu, hem proxy'yi hem de bir revoke fonksiyonunu içeren bir nesne döndürür.


const target = { data: 'hassas' };
const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.data); // Çıktı: hassas

// Şimdi, proxy'nin erişimini geri alıyoruz
revoke();

try {
  console.log(proxy.data); // Bu bir hata fırlatacaktır
} catch (e) {
  console.error(e.message);
  // Çıktı: Cannot perform 'get' on a proxy that has been revoked
}

Proxy'ler ve Diğer Metaprogramlama Teknikleri

Proxy'lerden önce, geliştiriciler benzer hedeflere ulaşmak için başka yöntemler kullandılar. Proxy'lerin nasıl karşılaştırıldığını anlamak faydalıdır.

`Object.defineProperty()`

Object.defineProperty(), belirli özellikler için getter ve setter'lar tanımlayarak bir nesneyi doğrudan değiştirir. Proxy'ler ise orijinal nesneyi hiç değiştirmez; onu sararlar.

Sonuç: Sanallaştırmanın Gücü

JavaScript Proxy API, sadece akıllı bir özellikten daha fazlasıdır; nesneleri nasıl tasarlayabileceğimiz ve onlarla nasıl etkileşim kurabileceğimiz konusunda temel bir değişimdir. Temel işlemleri yakalamamıza ve özelleştirmemize olanak tanıyarak, Proxy'ler güçlü desenlerin dünyasına kapı açar: sorunsuz veri doğrulama ve dönüşümünden, modern kullanıcı arayüzlerini güçlendiren reaktif sistemlere kadar.

Küçük bir performans maliyeti ve uyulması gereken bir dizi kuralla birlikte gelseler de, temiz, ayrık ve güçlü soyutlamalar oluşturma yetenekleri eşsizdir. Nesneleri sanallaştırarak, daha sağlam, sürdürülebilir ve etkileyici sistemler inşa edebilirsiniz. Bir dahaki sefere veri yönetimi, doğrulama veya gözlemlenebilirlik içeren karmaşık bir zorlukla karşılaştığınızda, bir Proxy'nin iş için doğru araç olup olmadığını düşünün. Alet çantanızdaki en zarif çözüm olabilir.