هنگام کار کردن با تابعها، جاوااسکریپت انعطافپذیری بینظیری را ارائه میدهد. تابعها میتوانند رد و بدل شوند، به عنوان شیء استفاده شوند و حالا ما خواهیم دید که چگونه فراخوانیها را بین تابعها ارسال کنیم و رفتار آنها را تغییر دهیم.
کش کردن پنهانی
فرض کنیم تابع slow(x) را داریم که از پردازنده خیلی کار میکشد اما نتیجههای آن همیشه ثابت هستند. به عبارتی دیگر، برای x یکسان همیشه نتیجهای یکسان را برمیگردند.
اگر تابع زیاد فراخوانی میشود، ممکن است بخواهیم که نتیجهها را کَش کنیم (به یاد بسپاریم) تا از مصرف زمان اضافی برای محاسبات دوباره جلوگیری کنیم.
اما به جای اینکه این قابلیت را به slow(x) اضافه کنیم یک تابع دربرگیرنده (wrapper) میسازیم که کش کردن را اضافه میکند. همانطور که خواهیم دید، مزایای زیادی از انجام این کار دریافت میکنیم.
کد اینگونه است و توضیحات به دنبال آن:
function slow(x) {
// اینجا میتواند یک کاری که پردازنده را زیاد مشغول میکند وجود داشته باشد
alert(`با ${x} فراخوانی شد`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // وجود داشت cache اگر چنین کلیدی در
return cache.get(x); // نتیجه را از آن بخوان
}
let result = func(x); // را فراخوانی کن func در غیر این صورت
cache.set(x, result); // و نتیجه را کش کن (به خاطر بسپار)
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // کش شده و نتیجه آن برگردانده شد slow(1)
alert( "Again: " + slow(1) ); // از کش برگردانده شد slow(1) نتیجه
alert( slow(2) ); // کش شده و نتیجه آن برگردانده شد slow(2)
alert( "Again: " + slow(2) ); // از کش برگردانده شد slow(2) نتیجه
در کد بالا cachingDecorator یک دکوراتور است: تابعی خاص که یک تابع دیگر را دریافت میکند و رفتار آن را تغییر میدهد.
ایده این است که ما میتوانیم cachingDecorator را برای هر تابعی فراخوانی کنیم و این تابع، دربرگیرنده کشکننده را برمیگرداند. این عالی است چون ما میتوانیم تابعهای زیادی داشته باشیم که از چنین خاصیتی استفاده کنند و تنها کاری که ما باید انجام دهیم، اعمال cachingDecorator روی آنها است.
با جدا کردن کش کردن از کد تابع اصلی، ما کد اصلی را هم سادهتر نگه داشتیم.
نتیجهی cachingDecorator(func) یک «دربرگیرنده» است: تابع function(x) که فراخوانی func(x) را در منطق کش کردن «میپوشاند»:
از یک کد بیرونی، تابع slow دربر گرفته شده کار یکسانی انجام میدهد. فقط یک جنبه کش کردن به رفتار این تابع اضافه شده است.
برای خلاصهسازی، چند مزیت در استفاده کردن از یک cachingDecorator به صورت جداگانه به جای تغییر کد خود slow وجود دارد:
- تابع
cachingDecoratorرا میتوان دوباره استفاده کرد. ما میتوانیم آن را روی تابع دیگری هم اعمال کنیم. - منطق کش کردن جدا است، این منطق پیچیدگی خود
slowرا افزایش نداد (اگر وجود داشت). - اگر نیاز باشد ما میتوانیم چند دکوراتور را ترکیب کنیم (دکوراتورهای دیگر پیروی خواهند کرد).
استفاده از “func.call” برای زمینه
دکوراتور کش کردن که در بالا گفته شد برای کار با متدهای شیء مناسب نیست.
برای مثال، در کد پایین worker.slow() بعد از دکور کردن کار نمیکند:
// کش کند worker.slow کاری خواهیم کرد که
let worker = {
someMethod() {
return 1;
},
slow(x) {
// کاری که به پردازنده خیلی فشار میآورد را اینجا داریم
alert("فراخوانی شده با " + x);
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(x); // (**)
cache.set(x, result);
return result;
};
}
alert( worker.slow(1) ); // متد اصلی کار میکند
worker.slow = cachingDecorator(worker.slow); // حالا کاری میکنیم که کش کند
alert( worker.slow(2) ); // Error: Cannot read property 'someMethod' of undefined !وای یک ارور
ارور در خط (*) اتفاق میافتد، خطی که تلاش میکند به this.someMethod دسترسی پیدا کند و شکست میخورد. میتوانید ببینید چرا؟
دلیلش این است که دربرگیرنده تابع اصلی را به عنوان func(x) در خط (**) فراخوانی میکند. و زمانی که اینگونه فرا خواند، تابع this = undefined را دریافت میکند.
اگر سعی میکردیم که این را اجرا کنیم هم مشکل یکسانی پیش میآمد:
let func = worker.slow;
func(2);
پس دربرگیرنده فراخوانی را به متد اصلی میفرستد اما بدون زمینه this. به همین دلیل ارور ایجاد میشود.
بیایید این را درست کنیم.
یک متد درون ساخت خاص برای تابعها وجود دارد به نام func.call(context, …args) که به ما این امکان را میدهد تا به صراحت با تنظیم کردن this یک تابع را فرا بخوانیم.
سینتکس اینگونه است:
func.call(context, arg1, arg2, ...)
این متد با دریافت اولین آرگومان به عنوان this و بقیه آنها به عنوان آرگومانهای تابع func را اجرا میکند.
برای اینکه ساده بگوییم، این دو فراخوانی تقریبا کار یکسانی را انجام میدهند:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
هر دوی آنها func را با آرگومانهای 1، 2 و 3 فراخوانی میکنند. تنها تفاوت این است که func.call مقدار this را هم برابر با obj قرار میدهد.
به عنوان مثال، در کد پایین ما sayHi را با زمینههای مختلفی از شیءها فراخوانی میکنیم: sayHi.call(user) تابع sayHi را با تنظیم کردن this=user اجرا میکند و خط بعدی this=admin را تنظیم میکند:
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// استفاده کنید "this" برای قرار دادن شیءهای متفاوت به عنوان call از
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
و اینجا ما از call برای فراخوانی say همراه با زمینه و عبارت داده شده استفاده میکنیم:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// قرار میگیرد و "سلام" اولین آرگومان میشود this در user
say.call( user, "سلام" ); // John: سلام
در این مورد ما، میتوانیم از call درون دربرگیرنده استفاده کنیم تا زمینه را در تابع اصلی تنظیم کنیم:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("فراخوانی شده با " + x);
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" حالا
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // حالا کاری میکنیم که کش کند
alert( worker.slow(2) ); // کار میکند
alert( worker.slow(2) ); // کار میکند، تابع اصلی را فراخوانی نمیکند (کش شده است)
حالا همه چیز درست است.
برای اینکه همه چیز را روشن کنیم، بیایید عمیقتر ببینیم که this چگونه تنظیم شده است:
- بعد از دکور کردن،
worker.slowهمان دربرگیرندهیfunction (x) { ... }است. - پس زمانی که
worker.slow(2)اجرا میشود، دربرگیرنده2را به عنوان آرگومان دریافت میکند وthis=workerاست (همان شیء قبل از نقطه). - درون دربرگیرنده، با فرض اینکه نتیجه هنوز کش نشده است،
func.call(this, x)مقدارthisکنونی (=worker) و آرگومان کنونی (=2) را در متد اصلی تنظیم میکند.
چند آرگومانی شدن
حالا بیایید cachingDecorator را جامعتر کنیم. تا حالا فقط با تابعهایی که یک آرگومان داشتند کار میکرد.
حالا چگونه متد worker.slow که چند آرگومان دارد را کش کنیم؟
let worker = {
slow(min, max) {
return min + max; // یک کاری که به پردازنده فشار میآورد
}
};
// باید فراخوانیهایی که آرگومانهای یکسانی دارند را یه خاطر بسپارد
worker.slow = cachingDecorator(worker.slow);
قبلا، برای یک آرگومان میتوانستیم از cache.set(x, result) برای ذخیره نتیجه و cache.get(x) برای دریافت آن استفاده کنیم. اما حالا باید نتیجه را برای ترکیبی از آرگومانها(min,max) ذخیره کنیم. ساختار Map فقط یک مقدار را به عنوان کلید دریافت میکند.
چند راهحل احتمالی وجود دارد:
- یک ساختار داده جدید شبیه map پیادهسازی کنیم (یا از شخص ثالث استفاده کنیم) که همهکاره است و چندکلیدی را ممکن میسازد.
- از mapهای پیچیده استفاده کنیم:
cache.set(min)یکMapخواهد بود که جفت(max, result)را ذخیره میکند. پس ما میتوانیمresultرا به صورتcache.get(min).get(max)دریافت کنیم. - دو مقدار را به یک مقدار تبدیل کنیم. در این مورد خاص، میتوانیم از رشته
"min,max"به عنوان کلیدMapاستفاده کنیم. برای انعطاف پذیری، میتوانیم یک تابع ترکیبسازی(hashing function) برای دکوراتور تعیین کنیم که میداند چگونه از چند مقدار یک مقدار بدست آورد.
برای بسیاری از موارد عملی، نوع سوم به اندازه کافی مناسب است پس ما با همان کار میکنیم.
همچنین ما باید نه تنها x بلکه تمام آرگومانها را در func.call قرار دهیم. بیایید یادآوری کنیم که در یک تابع function() میتوانیم یک شبهآرایه از آرگومانهای آن را با arguments دریافت کنیم پس func.call(this, ...arguments) باید جایگزین func.call(this, x) شود.
اینجا یک cachingDecorator قدرتمندتر داریم:
let worker = {
slow(min, max) {
alert(`فراخوانی شده با ${min},${max}`);
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.call(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) ); // کار میکند
alert( "Again " + worker.slow(3, 5) ); // یکسان است (کش شده)
حالا این تابع با هر تعداد آرگومان کار میکند (گرچه تابع ترکیبسازی هم باید جوری تنظیم شود که هر تعداد آرگومان را قبول کند. یک راه جالب برای کنترل این موضوع پایینتر پوشش داده شده است).
دو تفاوت وجود دارد:
- در خط
(*)این تابع،hashرا فراخوانی میکند تا یک کلید را ازargumentsبسازد. اینجا ما از تابع ساده «پیوند دادن» استفاده کردیم که آرگومانهای(3, 5)را به کلید"3,5"تبدیل میکند. موارد پیچیدهتر ممکن است تابعهای ترکیبسازی دیگری را نیاز داشته باشند. - سپس خط
(**)برای اینکه زمینه و تمام آرگومانهایی که دربرگیرنده دریافت کرد (نه فقط اولی) را در تابع اصلی قرار دهد ازfunc.call(this, ...arguments)استفاده میکند.
متد func.apply
میتوانستیم به جای func.call(this, ...arguments) از func.apply(this, arguments) استفاده کنیم.
سینتکس متد درونساخت func.apply اینگونه است:
func.apply(context, args)
این متد با تنظیم کردن this=context و استفاده از شیء args به عنوان لیستی از آرگومانها، تابع func را فراخوانی میکند.
تنها تفاوت بین call و apply این است که call لیستی از آرگومانها را قبول میکند در حالی که apply یک شیء شبهآرایه که شامل آرگومانها است را قبول میکند.
پس این دو فراخوانی تقریبا یکی هستند:
func.call(context, ...args);
func.apply(context, args);
آنها فراخوانی یکسانی از func همراه با زمینه و آرگومانهای داده شده را انجام میدهند.
فقط یک تفاوت جزئی در مورد args وجود دارد:
- سینتکس اسپرد
...به ما اجازه میدهد تاargsحلقهپذیر را به عنوان لیست درcallقرار دهیم. - متد
applyفقطargsشبهآرایه را قبول میکند.
…و برای شیءهایی که هم حلقهپذیر و هم شبهآرایه هستند، مانند آرایه واقعی، ما میتوانیم هر یک از آنها را استفاده کنیم اما احتمالا apply سریعتر باشد چون بیشتر موتورهای جاوااسکریپت آن را از دورن بهتر بهینه کردهاند.
قرار دادن تمام آرگومانها در کنار زمینه در تابعی دیگر را ارسال کردن فراخوانی(call forwarding) میگویند.
این سادهترین شکل از آن است:
let wrapper = function() {
return func.apply(this, arguments);
};
زمانی که یک کد بیرونی این wrapper را فراخوانی کند، نمیتوان آن را از فراخوانی تابع اصلی func تشخیص داد.
قرض گرفتن یک متد
حالا بیایید یک پیشرفت جزئی دیگر در تابع ترکیبسازی ایجاد کنیم:
function hash(args) {
return args[0] + ',' + args[1];
}
اکنون، این تابع فقط روی دو آرگومان کار میکند. اگر این تابع بتواند هر تعداد args را به هم بچسباند بهتر میشد.
راهحل طبیعی استفاده از متد arr.join است:
function hash(args) {
return args.join();
}
…متاسفانه این روش کار نخواهد کرد. چون ما در حال فراخوانی hash(arguments) هستیم و شیء arguments هم حلقهپذیر است و هم شبهآرایه اما یک آرایه واقعی نیست.
پس همانطور که در کد پایین میبینیم، فراخوانی join بر روی آن با شکست مواجه میشود:
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
اما هنوز یک راه آسان برای استفاده از پیوند دادن آرایه وجود دارد:
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
این ترفند قرضگیری متد (method borrowing) نام دارد.
ما متد پیوند دادن را از یک آرایه معمولی (قرض) میگیریم ([].join) و برای اجرای آن با زمینه arguments از [].join.call استفاده میکنیم.
این چرا کار میکند؟
به دلیل اینکه الگوریتم داخلی متد نیتیو (native method) arr.join(glue) بسیار ساده است.
این مراحل تقریبا «بدون تغییر» از مشخصات زبان برداشته شده است:
- فرض کنیم که
glueآرگومان اول باشد یا اگر آرگومانی وجود نداشت، پس یک کاما",". - فرض کنیم
resultیک رشته خالی باشد. this[0]را بهresultاضافه کنید.glueوthis[1]را اضافه کنید.glueوthis[2]را اضافه کنید.- …تا زمانی که تعداد
this.lengthالمان به هم چسبیدند ادامه دهید. resultرا برگردانید.
پس از لحاظ فنی این تابع this را دریافت میکند و this[0]، this[1] و بقیه را به هم پیوند میزند. این متد از قصد طوری نوشته شده است که هر this شبهآرایه را قبول کند (این اتفاقی نیست، بسیاری از متدها از این موضوع پیروی میکنند). به همین دلیل است که با this=arguments هم کار میکند.
دکوراتورها و ویژگیهای تابع
به طور کلی اینکه یک تابع یا متد را با تابعی دکور شده جایگزین کنیم مشکلی ایجاد نمیکند به جز در موردی کوچک. اگر تابع اصلی ویژگیهایی درون خود داشته باشد، مثلا func.calledCount یا هر چیزی، سپس تابع دکور شده آنها را فراهم نمیکند. چون یک دربرگیرنده است. پس اگر کسی از دکوراتورها استفاده کرد باید حواسش به این موضوع باشد.
برای نمونه، در مثال بالا اگر تابع slow ویژگیای درون خودش داشت، سپس cachingDecorator(slow) یک دربرگیرنده خواهد بود که آن ویژگی را ندارد.
بعضی از دکوراتورها ممکن است ویژگیهای خودشان را داشته باشند. مثلا یک دکوراتور میتواند تعداد دفعاتی که یک تابع فراخوانی شده و اجرای آن چقدر زمان برده است را محاسبه کند و از طریق ویژگیهای دربرگیرنده این اطلاعات را در اختیار بگذارد.
گرچه راهی برای ساخت دکوراتورهایی که به ویژگیهای تابع دسترسی داشته باشد وجود دارد اما این روش به استفاده از یک شیء Proxy خاص برای دربرگرفتن تابع نیاز دارد. ما این موضوع را بعدها در مقاله Proxy and Reflect بررسی میکنیم.
خلاصه
دکوراتور یک دربرگیرنده تابع است که رفتار آن را تغییر میدهد. کار اصلی هنوز توسط تابع انجام میشود.
دکوراتورها میتوانند به عنوان «خاصیتها» یا «جنبههایی» دیده شوند که میتوانند به تابع اضافه شوند. ما میتوانیم یک یا چند خاصیت اضافه کنیم. و همه اینها را بدون تغییر کد آن انجام دهیم.
برای پیادهسازی cachingDecorator، ما متدهای زیر را مطالعه کردیم:
- func.call(context, arg1, arg2…) – تابع
funcرا همراه با زمینه و آرگومانهای داده شده فرا میخواند. - func.apply(context, args) – با پاس دادن
contextبه عنوانthisو شبهآرایهargsدرون لیستی از آرگومانها، تابعfuncرا فراخوانی میکند.
ارسال فراخوانی به طور کل معمولا با apply انجام میشود:
let wrapper = function() {
return original.apply(this, arguments);
};
همچنین یک مثال از قرضگیری متد دیدیم، زمانی که ما یک متد را از شیءای قرض میگیریم و آن را با زمینهای از شیءای دیگر توسط call فراخوانی میکنیم. اینکه متدهای آرایه را دریافت کنیم و آنها را روی arguments اعمال کنیم بسیار رایج است. استفاده از شیء پارامترهای رست، راه جایگزین و یک آرایه واقعی است.
دکوراتورهای زیادی در واقعیت وجود دارد. با حل کردن تمرینهای این فصل نشان دهید که چقدر آنها را یاد گرفتهاید.
نظرات
<code>استفاده کنید، برای چندین خط – کد را درون تگ<pre>قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)