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.
- Target (Hedef): Bu, sarmalamak istediğiniz orijinal nesnedir. Diziler, fonksiyonlar ve hatta başka bir proxy dahil olmak üzere her tür nesne olabilir. Proxy bu hedefi sanallaştırır ve tüm işlemler sonuç olarak (zorunlu olmasa da) ona yönlendirilir.
- Handler (İşleyici): Bu, proxy için mantığı içeren bir nesnedir. Özellikleri 'trap' (yakalayıcı) olarak bilinen fonksiyonlar olan bir yer tutucu nesnedir. Proxy üzerinde bir işlem gerçekleştiğinde, handler üzerinde karşılık gelen bir trap arar.
- Traps (Yakalayıcılar): Bunlar, handler üzerinde özellik erişimi sağlayan metotlardır. Her trap, temel bir nesne işlemine karşılık gelir. Örneğin,
get
trap'i özellik okumayı,set
trap'i ise özellik yazmayı yakalar. Eğer bir trap handler üzerinde tanımlanmamışsa, işlem sanki proxy hiç yokmuş gibi hedefe yönlendirilir.
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.
target
: Orijinal nesne.property
: Erişilen özelliğin adı.receiver
: Proxy'nin kendisi veya ondan miras alan bir nesne.
Ö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.
value
: Özelliğe atanan yeni değer.- Trap, bir boolean döndürmelidir: atama başarılıysa
true
, değilsefalse
(bu, katı modda birTypeError
fırlatır).
Ö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.
target
: Orijinal fonksiyon.thisArg
: Çağrı içinthis
bağlamı.argumentsList
: Fonksiyona geçirilen argümanların listesi.
Ö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ı)
- İyi olduğu durumlar: Framework'ler ve kütüphaneler oluşturmak (örneğin, state yönetimi, ORM'ler), hata ayıklama ve loglama, sağlam doğrulama sistemleri uygulamak ve temel veri yapılarını soyutlayan güçlü API'ler oluşturmak.
- Alternatifleri düşünülmesi gereken durumlar: Performansı kritik algoritmalar, bir sınıfın veya bir fabrika fonksiyonunun yeterli olacağı basit nesne genişletmeleri veya ES6 desteği olmayan çok eski tarayıcıları desteklemeniz gerektiğinde.
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.
- Kapsam: `defineProperty` özellik bazında çalışır. İzlemek istediğiniz her özellik için bir getter/setter tanımlamanız gerekir. Bir Proxy'nin
get
veset
trap'leri geneldir ve daha sonra eklenen yeniler de dahil olmak üzere herhangi bir özellik üzerindeki işlemleri yakalar. - Yetenekler: Proxy'ler, `deleteProperty`,
in
operatörü ve fonksiyon çağrıları gibi `defineProperty`'ın yapamayacağı daha geniş bir işlem yelpazesini yakalayabilir.
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.