JavaScript fonksiyonlar ile uğraşırken inanılmaz derecede esneklik sağlamaktadır. Fonksiyonlar başka fonksiyonlara gönderilebilir, obje olarak kullanılabilir. Şimdi ise bunların nasıl iletileceği ve nasıl dekore edileceğinden bahsedilecektir;
Saydam Saklama
Diyelim ki slow(x) diye yoğun işlemci gücüne ihtiyaç duyan bir fonksiyonunuz olsun, buna rağmen sonucları beklediğiniz şekilde vermekte.
Eğer bu fonksiyon sık sık çağırılıyor ise farklı x’ler için sonucu saklamak bizi tekrar hesaplamadan kurtarabilir.
Fakat bunu slow() fonksiyonunun içine yazmak yerine yeni bir wrapper yazmak daha iyi olacaktır. Göreceğiniz üzere size oldukça fazla yardımı olacaktır.
Kod aşağıdaki gibidir:
function slow(x) {
// burada baya yoğun işlemci gücüne ihtiyaç duyan işler yapılmaktadır.
alert(`${x} ile çağırıldı`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // eğer sonuç map içerisinde ise
return cache.get(x); // değeri gönder
}
let result = func(x); // aksi halde hesap yap
cache.set(x, result); // sonra sonucu sakla
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) saklandı
alert( "Tekrar: " + slow(1) ); // aynısı döndü
alert( slow(2) ); // slow(2) saklandı
alert( "Tekrar: " + slow(2) ); // bir önceki ile aynısı döndü.
Yuarkıdaki kodda cachingDecorator bir dekoratör’dür: Diğer bir fonksiyonu alan ve bunun davranışını değiştiren özel bir fonksiyon.
Aslında her bir fonksiyon için cachingDecorator çağrılabilir ve o da saklama mekanizmasını kullanır. Harika, bu şekilde ihtiyacı olacak birçok fonksiyonumuz olabilir. Tek yapmamız gereken bu fonksiyonlara cachingDecorator uygulamak.
Saklama olayını ana fonksiyonldan ayırarak aslında daha temiz bir yapıya da geçmiş olduk.
Detayına inmeye başlayabiliriz.
cachingDecorator(func) bir çeşit “wrapper(saklayıcı)”'dır. Bu işlem func(x) i “saklama” işine yarar.
Gördüğünüz gibi, saklayıcı func(x)'ı olduğu gibi dönderir. Saklayıcının dışındaki slow olan fonksiyon hala aynı şekilde çalışır. Aslında davranışın üstüne sadece saklama(caching) mekanizması gelmiştir.
Özetlersek, ayrı bir cachingDecorator kullanmanın faydaları şu şekildedir:
cachingDecoratortekrar kullanılabilir. Başka bir fonksiyona da uygulanabilir.- Saklama(caching) mantığı ayrılmıştır böylece
slowkodun içine daha fazla kod yazıp karışması önlenmiştir. - İhtiyaç halinde birden fazla dekoratör birlikte kullanılabilir.
Kaynak için “func.all” kullanmak.
Yukarıda bahsettiğimiz saklama dekoratörü obje metodları ile çalışmak için müsait değildir.
Örneğin aşağıdaki kodda user.format() dekorasyondan sonra çalışmayı durdurur:
// worker.slow sakla yapılacaktır.
let worker = {
someMethod() {
return 1;
},
slow(x) {
// burada çok zorlu bir görev olabilir.
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
// eskisiyle aynı kod
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
alert( worker.slow(1) ); // orjinal metod çalışmakta
worker.slow = cachingDecorator(worker.slow); // şimdi saklamaya alındı.
alert( worker.slow(2) ); // Whoops! Error: Özellik okunamamaktadır. `someMethod` tanımsız.
(*) satırında hata olur this.someMethod’a erişmeye çalışır fakat başırılı olamaz. Nedeni ne olabilir ?
Sebebi (**) satırında orjinal func(x) çağırılmıştır. Bu şekilde çağırıldığında, fonksiyon this = undefined alır.
Aşağıdaki kod çalıştırılırsa da aynısı görülebilir:
let func = worker.slow;
func(2);
Saklayıcı çağrıyı gerçek çalışacak metoda gönderir. Fakat this olmadığından dolayı hata alır.
Bunu düzeltmek için.
Özel bir metod bulunmaktadır func.call(context, …args) this’i belirterek doğrudan fonksiyonu çağırmaya yarar.
Yazımı aşağıdaki gibidir:
func.call(context, arg1, arg2, ...)
İlk argüman this’dir diğerleri ise fonksiyon için gerekli argümanlardır.
Kullanımı şu şekildedir:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
Her ikisi de aslında func fonksiyonlarını 1, 2, 3 argümanları ile çağırır tek fark func.call fonksiyonunda thisde gönderilir.
Örneğin, aşağıdaki kod sayHi metodunu iki farklı objeye değer atayarak çağırır. Birinci satırda this=user ikinci satırda ise this=admin değeri atanarak bu çağrı gerçekleştirilir.
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// farklı objeler "this" objesi olarak gönderilebilir.
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
Burada say metodunu çağırarak ne söyleneceğini gönderiyoruz:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// user `this` olmakta ve `phrase` ilk argüman olmaktadır.
say.call( user, "Hello" ); // John: Hello
Bizim durumumuzda saklayıcı içinde call kullanarak içeriği orijinal fonksiyona aktarabiliriz:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert(x + "ile çağırıldı");
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // "this" is passed correctly now
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // çalışır
alert( worker.slow(2) ); // orjinali değilde hafızadaki çalışır.
Şimdi her şey beklendiği gibi çalışıyor.
Daha açıklayıcı olması için this’in nasıl ilerlediğini inceleyebiliriz:
- Dekorasyon işleminden sonra
worker.slowartıkfunction(x){ ...}halini almıştır. - Öyleyse
worker.slow(2)çalıştırıldığında saklayıcı2vethis=worker( noktadan önceki obje ) argümanlarını alır. - Saklayıcı(wrapper) içinde sonucun henüz belleğe alınmadığını varsayarsak
func.call(this,x)o ankithis(=worker) ve ('=2`) değerini orjinal metoda gönderir.
“func.apply” ile çoklu argüman kullanımı
cachingDecorator daha evrensel yapmak için ne değişiklikler yapmalıdır?
let worker = {
slow(min, max) {
return min + max; // CPU'ya çok yük bindiren bir işlem.
}
};
// aynı argüman ile çağırılmalıdır.
worker.slow = cachingDecorator(worker.slow);
Burada çözmemiz gereken iki problem bul
İlki min ve max değerlerinin bu bellek haritasında anahtar olarak nasıl tutulacağı. Önceki konuda tek x argümanı için cache.set(x,result) şeklinde sonucu belleğe kaydetmiş ve sonra cache.get(x) şeklinde almıştık. Fakat şimdi sonucu argümanların birleşimi şeklinde hatırlamak gerekmektedir. Normalde Map anahtarı tek değer olarak almaktadır.
Bu sorunun çözümü için bazı çözümler şu şekildedir:
- Map-benzeri bir yapı kurarak birkaç anahtarı kullanabilen bir veri yapısı oluşturmak.
- İç içe map kullanarak; Örneğin
cache.set(min)aslında(max, result)'ı tutmaktadır. Böyleceresultcache.get(min).get(max)şeklinde alınabilir. - İki değeri teke indirerek. Bizim durumumuzda bu
"min,max"şeklinde bir karakter dizisiniMap’in anahtarı yapmak olabilir. Ayrıca hashing fonksiyonu’u dekoratöre sağlayabiliriz. Bu fonksiyon da birçok değerden bir değer yapabilir.
Çoğu uygulama için 3. çözüm yeterlidir. Biz de bu çözüm ile devam edeceğiz.
İkinci görev ise fonksiyona birden fazla argümanın nasıl gönderileceğidir. Şu anda saklayıcı fonksiyona function(x) şeklinde tek argüman gönderilmektedir. Bu da func.call(this,x) şeklinde uygulanır.
Burada kullanılacak diğer metod func.apply’dır.
Yazımı:
func.apply(context, args)
Bu func’ı this=context ve args için dizi benzeri bir argüman dizisi ile çalıştırır.
Örneğin aşağıdaki iki çağrı tamamen aynıdır.
func(1, 2, 3);
func.apply(context, [1, 2, 3])
Her ikisi de func’ı 1,2,3argümanları ile çalıştırır. Fakat apply ayrıca this=context’i ayarlar.
function say(time, phrase) {
alert(`[${time}] ${this.name}: ${phrase}`);
}
let user = { name: "John" };
let messageData = ['10:00', 'Hello']; // time, phrase'e dönüşür.
// this = user olur , messageData liste olarak (time,phrase) şeklinde gönderilir.
say.apply(user, messageData); // [10:00] John: Hello (this=user)
call argüman listesi beklerken apply dizi benzeri bir obje ile onları alır.
Yayma operatörü Gerisi parametreleri ve yayma operatörleri konusunda ... yayma operatörünün ne iş yaptığını işlemiştik. Dizilerin argüman listesi şeklinde gönderilebileceğinden bahsemiştik. Öyleyse call ile bunu kullanırsak neredeyse apply’ın işlevini görebiliriz.
Aşağıdaki iki çağrı birbirinin aynısıdır:
let args = [1, 2, 3];
func.call(context, ...args); // dizileri yayma operatörü ile liste şeklinde gönderir.
func.apply(context, args); // aynısını apply ile yapar.
İşleme daha yakından bakılacak olursa call ile apply arasında oldukça küçük bir fark vardır.
- Yayma operatörü
...list gibi döngülenebilir argümanlarıcalledilmek üzere iletebilir. applyise sadece dizi-benzeriargsalır.
Öyleyse bu çağrılar birbirinin tamamlayıcısıdır. Döngülenebilir beklediğimizde call, dizi-benzeri beklediğimizde ise apply çalışır.
Eğer args hem döngülenebilir bende dizi ise teknik olarak ikisini de kullanabiliriz, fakat apply muhtemelen daha hızlı olacaktır. Çünkü tek bir işlemden oluşur. Çoğu JavaScript motoru bir kaç call + spread kullanmaktan daha iyi şekilde optimizasyon yapar.
Apply’ın en çok çağrıyı diğer fonksiyona iletirken işe yarar:
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};
Buna çağrı iletme denir. Saklayıcı sahip olduğu her şeyi iletir: this ile argümanları anotherFunction’a iletir ve sonucunu döner.
Böyle bir saklayıcı kod çağırıldığında içerideki orjinal fonksiyon çağıran tarafından ayrıştırılamaz.
Şimdi bunları daha güçlü cachingDecoratır’da işleyelim:
let worker = {
slow(min, max) {
alert(`${min},${max} ile çağırıldı`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.apply(this, arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
Şimdi saklayıcı(wrapper) birçok argüman ile çalışabilir.
İki tane değişiklik oldu:
(*)satırında birhasile argümanlardan tek bir anahtar meydana getirildi. Bunun için basit “birleştirme” fonksiyonu kullanılmıştır.(3,5)"3,5"şekline getirildi. Tabi başka hash fonksiyonları için daha karmaşık bir yapı gerekebilir.(**)satırında isefunc.applyile hem kaynak ( this ) hem de saklayıcı argümanları (ne kadar olduğu önemli değil) orjinal fonksiyona iletilmiştir.
Metod Ödünç Alma
Hash fonksiyonunda biraz değişiklik yapalım:
function hash(args) {
return args[0] + ',' + args[1];
}
Bundan sonra bu fonksiyon sadece iki argümanla çalışacak. Bunun yerine belirsiz sayıdaki argümanla çalışsa daha iyi olur.
Bunu kullanmanın doğal yolu arr.join metodudur:
function hash(args) {
return args.join();
}
… Malesef bu çalışmaz. Çünkü hash(argümanlar) çağırılmakta ve arguments objei hem döngülenebilir hemde dizi-benzeri olduğundan, fakat gerçek dizi olmadığından çalışmaz.
Öyleyse join bu durumda çağırılamaz, örneğin:
function hash() {
alert( arguments.join() ); // Error: arguments.join fonksiyon değil.
}
hash(1, 2);
Fakat yine de dizi birleştirme kullanmanın kolay bir yolu vardır.
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
Bu cambazlığa metod ödünç alma denir.
Normal diziden [].join join ödünç alınır. Sonrasında arguments contexi ile çağrı yapılır [].join.call
Peki neden çalışır?
Çünkü gerçek dizinin join metodu oldukça basittir.
Tanımından “olduğu” gibi alındığında işlem şu şekildedir:
yapistirilk argüman olsun, eğer argüman yoksa","ilk argüman olsun.sonucboş karakter dizisi olsunthis[0]'ı sonuca ekle.yapistirvethis[1]'i ekleyapistirvethis[2]'i ekle- …
this.length’e kadarki tüm elemanlar yapıştırılana kadar ekle. sonucdön- Return
result.
Teknik olarak this’i alır ve this[0], this[1] …vs. şeklinde birleştirir. Bu şekilde yazılarak tüm dizi-gibi’lerin this şeklinde çalışmasını sağlar. Bundan dolayı this=arguments şeklinde de çalışır.
Özet
Decoratör fonksiyonun davranışını değiştiren saklayıcılardır(wrapper). İş hala ana fonksiyonda yapılır.
Genelde gerçek fonksiyonu dekoratör ile değiştirmek güvenlidir, bir olay haricinde. Eğer orjinal fonksiyon func.calledCount gibi bir özelliğe sahipse, dekoratör bunu sağlamayacaktır çünkü bu bir saklayıcıdır. Bundan dolayı kullanırken dikkatli olunmalıdır. Bazı dekoratörler kendine ait özellikler tutarlar.
cachingDecorator kullanmak için bazı metodlar denedik:
- func.call(context, arg1, arg2…) –
func’ı verilen kaynak ve argümanlar ile çağırır. - func.apply(context, args) –
kaynak’ıthisolarak vearray-benzeriargümanları liste olarak iletir.
çağrı iletme genelde apply ile gerçekleştirilir:
let wrapper = function() {
return original.apply(this, arguments);
}
Ayrıca metod ödünç alma’yı da gördük. Bir metotdan obje alındığında ve bu diğer objenin kaynağında(context) çağırıldığında gerçekleşir. Dizi metodlarını alıp argümanlara uygulama çokça kullanılır. Bunun alternatifi geriye kalan parametre objelerini kullanmaktır. Bunlar gerçek dizilerdir.
Araştırırsanız birçok dekoratör görebilirsiniz. Bu bölümdeki görevleri çözerekte kendinizi geliştirebilirsiniz.
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)