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çinget, bir özelliÄe yazmak içinsetgibi.
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.
proxy.test=yazma iÅlemi, deÄeritargetüzerine yazar.proxy.testokuma iÅlemi, deÄeritargetâtan döndürür.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ı |
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ıysatrue, deÄilsefalsedöndürmelidir.[[Delete]]baÅarılıysatrue, deÄilsefalsedö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 koddathisolarak kullanılacak nesnedir. Genellikle buproxynesnesinin 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)
target yerine kullanılmalıdırProxyâ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,receiverâgettuzaÄı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ınYukarı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:
getokuma sırasında hata fırlatmak için,setyazma sırasında hata fırlatmak için,deletePropertysilme sırasında hata fırlatmak için,ownKeys_ile baÅlayan özelliklerifor...indöngüsü veyaObject.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.
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)
targetânew 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:
targethedef nesne (fonksiyon),thisArgçaÄrıda kullanılacakthisdeÄeri,argsargümanların listesi.
ÃrneÄin, Dekoratörler ve iletilme, call/apply bölümünde yaptıÄımız delay(f, ms) dekoratörünü hatırlayalım.
Orada proxy kullanmadan yapmıÅtık. delay(f, ms) çaÄrısı, tüm çaÄrıları ms milisaniye sonra fâe ileten bir fonksiyon döndürüyordu.
Fonksiyon-tabanlı uygulama:
// proxy yok, sadece bir sarmalayıcı fonksiyon
function delay(f, ms) {
// timeout sonrası çaÄrıyı f'ye ileten bir sarmalayıcı döndür
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// artık sayHi çaÄrıları 3 saniye gecikmeli
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (3 saniye sonra)
GördüÄünüz gibi, çoÄunlukla çalıÅıyor. (*) satırındaki sarmalayıcı fonksiyon, çaÄrıyı bekleme süresinden sonra gerçekleÅtiriyor.
Ama sarmalayıcı fonksiyon, özellik okuma/yazma gibi diÄer iÅlemleri iletmez. Dolayısıyla, orijinal fonksiyonun bir özelliÄi varsa, sarmalamadan sonra ona eriÅemeyiz:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length argüman sayısıdır)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (sarmalayıcının argümanı yok)
Proxy çok daha güçlüdür; çünkü her Åeyi hedef nesneye iletir.
Sarmalayıcı fonksiyon yerine Proxy kullanalım:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy "length" okuma iÅlemini hedefe iletir
sayHi("John"); // Hello, John! (3 saniye sonra)
Sonuç aynı, ancak artık yalnızca çaÄrılar deÄil, proxy üzerindeki tüm iÅlemler de orijinal fonksiyona iletiliyor. Bu nedenle (*) satırında sarmalamadan sonra sayHi.length doÄru Åekilde döndü.
Böylece daha âzenginâ bir sarmalayıcı elde etmiÅ olduk.
BaÅka trapâler de var, fakat sanırım artık mantıÄı anladınız.
Reflect
Reflect APIâsi, Proxy ile birlikte çalıÅmak üzere tasarlanmıÅtır.
Yakalanabilecek (trapâlenebilecek) her dahili nesne iÅlemi için bir Reflect metodu vardır.
Bu metodun adı ve parametreleri, ilgili trap ile aynıdır ve iÅlemi hedef nesneye iletmek için kullanılabilir.
ÃrneÄin:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop} TO ${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // GET name
user.name = "Pete"; // SET name TO Pete
Reflect.get, tıpkıtarget[prop]gibi özelliÄi okur.Reflect.set, tıpkıtarget[prop] = valuegibi özelliÄi yazar, ayrıca doÄru dönüŠdeÄerini de garanti eder.
ÃoÄu durumda Reflect kullanmadan da aynı iÅi yapabiliriz, ancak bazı ince detayları kaçırabiliriz.
AÅaÄıdaki örneÄe bakalım; Reflect kullanılmamıŠve yanlıŠsonuç veriyor:
Bir user nesnesini proxyâliyoruz, sonra ondan kalıtım alıp bir getter kullanıyoruz:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
user = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*)
}
});
let admin = {
__proto__: user,
_name: "Admin"
};
// Beklenen: Admin
alert(admin.name); // Guest (?!?)
GördüÄünüz gibi sonuç hatalı! admin.name deÄerinin "Admin" olması gerekirken "Guest" döndü. Proxy olmadan "Admin" olacaktı; proxyleme nesneyi âbozmuÅâ gibi görünüyor.
Peki neden? Son satırdaki çaÄrının ne yaptıÄına bakalım:
adminiçindenameözelliÄi yok, bu yüzden çaÄrıadminâin prototipine gider.- Prototip bir proxy olduÄu için,
gettuzaÄınameokuma giriÅimini yakalar. (*)satırındatarget[prop]döndürülür, pekitargetnedir?target,getâin ilk parametresidir ve her zamannew Proxyâye verilen orijinal nesnedir(user).- Dolayısıyla,
target[prop]getternameâithis=target=userolarak çaÄırır. - Bu yüzden sonuç
"Guest"olur.
Bunu nasıl düzeltiriz? İÅte bu noktada üçüncü parametre olan receiver devreye girer! receiver, doÄru this deÄerini tutar. Sadece Reflect.getâi çaÄırarak bu deÄeri aktarabiliriz.
DoÄru çözüm Åöyle olur:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
user = new Proxy(user, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: user,
_name: "Admin"
};
alert(admin.name); // Admin
Artık receiver, doÄru this deÄerini tutarak getterâa Reflect.get aracılıÄıyla ((*) satırında) aktarılır ve her Åey düzgün çalıÅır.
Ayrıca tuzaÄı Åu Åekilde de yazabilirdik:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect çaÄrıları, tuzaklarla birebir aynı isimlere ve argümanlara sahiptir.
Bu özellikle böyle tasarlanmıÅtır.
Dolayısıyla return Reflect... Åeklinde bir çaÄrı, iÅlemi güvenli bir Åekilde iletmenin ve hiçbir detayı unutmamanın en kolay yoludur.
Proxy Sınırlamaları
Proxyâler, var olan nesnelerin (hatta yerleÅik olanların, örn. diziler) davranıÅlarını deÄiÅtirmek veya özelleÅtirmek için mükemmel bir araçtır.
Yine de, bazı sınırlamaları vardır.
YerleÅik Nesneler: İçsel Slotlar (Internal Slots)
Birçok yerleÅik nesne, örneÄin Map, Set, Date, Promise ve benzerleri, âiçsel slotâ (internal slot) denen yapıları kullanır.
Bunlar özelliklere benzer, ancak dahili amaçlar için ayrılmıÅtır.
YerleÅik metotlar bu slotlara doÄrudan eriÅir, [[Get]]/[[Set]] gibi içsel metotları kullanmazlar. Bu yüzden Proxy bunları yakalayamaz.
âEee ne olmuÅ, sonuçta dahili yapılar!â diyebilirsiniz.
Ama sorun Åudur: Böyle bir yerleÅik nesne proxylenirse, proxy bu içsel slotlara sahip olmaz ve dolayısıyla yerleÅik metotlar çalıÅmaz.
ÃrneÄin:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Hata
Bir Map üzerinde set çaÄırmak baÅarısız olur; çünkü bu davranıŠiçsel implementasyona baÄlıdır.
Dahili olarak bir Map, tüm verilerini [[MapData]] adlı içsel slotta saklar. Proxyâde böyle bir slot yoktur. set metodu this.[[MapData]]âya eriÅmeye çalıÅır; ancak this=proxy olduÄu için bulamaz ve hata verir.
Neyse ki bunu düzeltmenin bir yolu var:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (çalıÅıyor!)
Åimdi düzgün çalıÅıyor, çünkü get tuzaÄı fonksiyon özellikleri (map.set gibi), hedef nesneye (map) baÄlıyor.
Böylece proxy.set(...) içindeki this artık proxy deÄil, orijinal map olur. Dolayısıyla set metodu this.[[MapData]] slotuna eriÅmeye çalıÅtıÄında baÅarılı olur.
Arrayâin içsel slotları yokturBelirgin bir istisna: yerleÅik Array içsel slotlar kullanmaz. Bu tarihsel bir sebepten dolayıdır, çok eski bir yapı olduÄundan.
Bu nedenle, dizileri proxyâlemek bu tür bir soruna yol açmaz.
Ãzel Alanlar (Private Fields)
Benzer bir durum, sınıflardaki özel (private) alanlarda da görülür.
ÃrneÄin, getName() metodu özel #name alanına eriÅir, ancak proxy uygulandıktan sonra bozulur:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
Bunun nedeni, özel alanların (private fields) dahili slotlar (internal slots) kullanılarak uygulanmasıdır.
JavaScript, bunlara eriÅirken [[Get]] veya [[Set]] mekanizmalarını kullanmaz.
user.getName() çaÄrısında, this deÄeri proxylanmıŠkullanıcı nesnesidir ve bu nesnede özel alanların bulunduÄu slot yoktur.
Daha önceki örneklerde olduÄu gibi, metodu hedef nesneye baÄlayarak (bind) bu durumu düzeltebiliriz:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
Ancak bu çözümün daha önce açıkladıÄımız bir dezavantajı var: Metot, orijinal nesneye doÄrudan eriÅim kazanır ve onu baÅka bir yere aktarabilir. Bu da diÄer proxy iÅlevselliklerini bozabilir.
Proxy != target
Proxy ve orijinal nesne farklı nesnelerdir. Bu oldukça doÄaldır, deÄil mi?
Yani bir nesneyi bir yerde saklayıp daha sonra proxyâlarsak, bazı Åeyler bozulabilir:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
GördüÄünüz gibi, proxylamadan sonra user, allUsers kümesinde bulunamıyor çünkü proxy farklı bir nesne.
=== yakalayamazProxyâler new (-> construct), in (-> has), delete (-> deleteProperty) gibi birçok iÅlemi yakalayabilir.
Ancak, nesneler için sıkı eÅitlik testi (===) yakalanamaz.
Bir nesne yalnızca kendisine eÅittir; baÅka hiçbir Åeye deÄil.
Bu nedenle, nesneleri karÅılaÅtıran tüm iÅlemler ve yerleÅik sınıflar, nesne ile proxy arasında ayrım yapar. Yani proxyâler tam anlamıyla âÅeffafâ bir yedek olamaz.
Geri Alınabilir Proxyâler (Revocable Proxies)
Revocable proxy, yani âgeri alınabilir proxyâ, devre dıÅı bırakılabilen bir proxy türüdür.
Diyelim ki bir kaynaÄımız var ve istediÄimiz anda bu kaynaÄa eriÅimi kapatmak istiyoruz.
Bunu, herhangi bir trap eklemeden bir ârevocable proxyâ ile sarmalayabiliriz.
Bu proxy iÅlemleri hedef nesneye iletir, ancak ayrıca onu devre dıÅı bırakmak için özel bir fonksiyon saÄlar.
Sözdizimi Åöyledir:
let {proxy, revoke} = Proxy.revocable(target, handler)
Bu çaÄrı, iki özellik döndürür:
proxyvekil nesne,revokebu proxyâyi devre dıÅı bırakmak için kullanılan fonksiyon.
Ãrnek:
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// nesne yerine proxyâyi bir yere aktarabiliriz...
alert(proxy.data); // Valuable data
// kodun ilerleyen bir kısmında
revoke();
// proxy artık çalıÅmaz (devre dıÅı)
alert(proxy.data); // Hata
revoke() çaÄrısı, proxyânin hedef nesneyle olan tüm dahili baÄlantılarını kaldırır.
Böylece artık birbirleriyle iliÅkili deÄildirler.
Bu noktadan sonra hedef nesne, çöp toplayıcı (garbage collector) tarafından silinebilir.
Ayrıca revoke fonksiyonunu bir WeakMap içinde saklayarak, proxy üzerinden kolayca bulabiliriz:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ...kodun ilerleyen kısmında...
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Hata (revoked)
Bu yaklaÅımın avantajı, revoke fonksiyonunu her yere taÅımak zorunda olmamamızdır.
Proxy üzerinden WeakMap aracılıÄıyla gerektiÄinde eriÅebiliriz.
Burada Map yerine WeakMap kullanmamızın nedeni, çöp toplamayı engellememesidir.
EÄer bir proxy nesnesine artık eriÅilmiyorsa (örneÄin hiçbir deÄiÅken onu tutmuyorsa), WeakMap onun bellekten silinmesine izin verir. Ãünkü artık revoke fonksiyonuna da ihtiyacımız yoktur.
Referanslar
Ãzet
Proxy, bir nesnenin etrafını saran ve iÅlemleri o nesneye yönlendiren (ve istenirse bazılarını yakalayan) bir yapıdır.
Her tür nesneyi, sınıflar ve fonksiyonlar dahil, sarmalayabilir.
Sözdizimi:
let proxy = new Proxy(target, {
/* tuzaklar */
});
â¦Bundan sonra target yerine her yerde proxy kullanılmalıdır. Bir proxyânin kendi özellikleri veya metotları yoktur.
Bir iÅlem, uygun bir trap tanımlanmıÅsa yakalanır; yoksa hedef nesneye (target) iletilir.
Yakalanabilecek iÅlemler Åunlardır:
- Ãzellik okuma (
get), yazma (set), silme (deleteProperty), hatta var olmayan bir özellik bile. - Fonksiyon çaÄrıları:
newile (constructtuzaÄı),newolmadan (applytuzaÄı). - Daha birçok iÅlem (tam liste makalenin baÅında ve MDN dokümentasyonunda yer alır).
Bu sayede:
- âSanalâ (virtual) özellikler ve metotlar oluÅturabiliriz,
- Varsayılan deÄerler tanımlayabiliriz,
- Gözlemlenebilir (observable) nesneler oluÅturabiliriz,
- Fonksiyon dekoratörleri (decorators) yazabiliriz, ve çok daha fazlasını yapabiliriz.
Bir nesneyi farklı Proxy katmanlarıyla birden fazla kez sarmalayabiliriz. Her biri farklı bir iÅlevsellik ekleyebilir.
Reflect APIâsi Proxyâyi tamamlamak üzere tasarlanmıÅtır. Her Proxy tuzaÄı için aynı argümanları alan bir Reflect çaÄrısı bulunur. Bu çaÄrılar, iÅlemleri hedef nesneye güvenli bir Åekilde iletmek için kullanılmalıdır.
Proxyâlerin bazı sınırlamaları vardır:
- YerleÅik nesneler, âiçsel slotâ (internal slot) adı verilen alanlara sahiptir; bunlara eriÅim proxy ile yakalanamaz. (Yukarıda çözüm örneÄi verilmiÅtir.)
- Aynı durum özel (private) sınıf alanları için de geçerlidir; bunlar da slotâlar aracılıÄıyla uygulanır. Bu nedenle proxylanmıŠmetod çaÄrıları, bu alanlara eriÅebilmek için
thisdeÄerinin hedef nesne olması gerekir. - Nesne eÅitlik testi (
===) yakalanamaz. - Performans: Proxy üzerinden özellik eriÅimi genellikle birkaç kat daha yavaÅtır. Ancak pratikte bu fark yalnızca dar boÄaz oluÅturan nesnelerde hissedilir.
Yorumlar
<code>kullanınız, birkaç satır eklemek için ise<pre>kullanın. EÄer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz)