هنگام کار کردن با تابعها، جاوااسکریپت انعطافپذیری بینظیری را ارائه میدهد. تابعها میتوانند رد و بدل شوند، به عنوان شیء استفاده شوند و حالا ما خواهیم دید که چگونه فراخوانیها را بین تابعها ارسال کنیم و رفتار آنها را تغییر دهیم.
کش کردن پنهانی
فرض کنیم تابع 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
اعمال کنیم بسیار رایج است. استفاده از شیء پارامترهای رست، راه جایگزین و یک آرایه واقعی است.
دکوراتورهای زیادی در واقعیت وجود دارد. با حل کردن تمرینهای این فصل نشان دهید که چقدر آنها را یاد گرفتهاید.