بستارها (Closures)
بستارها (Closures) یکی از ویژگیهای جالب، قدرتمند و در عین حال پیچیده در جاوا اسکریپت هستند. اما قبل از پرداختن به مفهوم بستار، لازم است کمی در مورد توابع تو در تو و ویژگیهای آنها در جاوا اسکریپت صحبت کنیم.
توابع تو در تو
در جاوا اسکریپت این امکان وجود دارد که توابع را به صورت تو در تو تعریف کنیم. مثلاً در قطعه کد زیر، تابع inner، داخل بدنهی تابع outer تعریف شده است.
function outer(){
let outside = 'Outside';
function inner(){
let inside = 'Inside';
console.log(inside);
console.log(outside);
}
console.log(outside);
inner();
}
در چنین شرایطی تابع outer مانند یک تابع سراسری در تمام نقاط برنامه در دسترس است. اما تابع inner فقط در بدنهی تابع outer قابل فراخوانی است. بنابراین فراخوانی تابع outer، خروجی زیر را تولید خواهد کرد.
outer();
← "Outside"
← "Inside"
← "Outside"
واضح است که اولین رشتهی "Outside" حاصل از اجرای خط 8 از تابع outer است. سپس با فراخوانی تابع inner در انتهای تابع outer، به ترتیب خطوط 5 و 6 اجرا شده و دو رشتهی "Inside" و "Outside" را در کنسول نمایش میدهند. توجه کنید که در توابع تو در تو، تابع درونی (inner) به متغیرهای تابع بیرونی (outer) دسترسی دارد. به همین دلیل امکان استفاده از متغیر outside در خط 6 وجود دارد. اما تابع بیرونی به متغیرهای تابع درونی دسترسی ندارد.
حال اگر تابع inner را به صورت مجزا و در حوزهی سراسری برنامه فراخوانی کنیم، با خطای زیر مواجه میشویم. زیرا تابع inner فقط در درون تابع outer قابل استفاده است.
inner();
← ReferenceError: inner is not defined
پس تابع inner را نمیتوان به صورت مستقیم در حوزهی سراسری فراخوانی کرد. اما با یک ترفند ساده و اعمال یک تغییر کوچک در تابع outer، میتوان تابع inner را به صورت غیر مستقیم در حوزهی سراسری فراخوانی کرد.
میدانیم که توابع میتوانند مقداری را به عنوان نتیجه بازگردانند. و مقدار بازگشتی از توابع میتواند از هر نوع دادهای باشد. در نتیجه خروجی یک تابع میتواند یک تابع دیگر باشد. به عنوان مثال میتوان تابع outer را به شکل زیر اصلاح کرد تا تابع inner را به عنوان نتیجه بازگرداند.
function outer(){
let outside = 'Outside';
function inner(){
let inside = 'Inside';
console.log(inside);
console.log(outside);
}
return inner;
}
در این صورت میتوان تابع inner را حتی خارج از بدنهی تابع outer فراخوانی کرد. برای این منظور کافی است یک بار تابع outer را فراخوانی کرده و مقدار بازگشتی از آن را در یک متغیر ذخیره کنیم. دستور زیر یک اشارهگر به تابع inner را در متغیری به نام myFunc ذخیره میکند.
let myFunc = outer();
حال با استفاده از متغیر myFunc میتوان تابع inner را خارج از بدنهی تابع outer نیز فراخوانی کرد. قطعه کد زیر نتیجهی این فراخوانی را نشان میدهد.
myFunc();
← "Inside"
← "Outside"
بستار (Closure)
حال به بحث اصلی این بخش، یعنی بستارها باز میگردیم. یک بستار تابعی است که به متغیرهای موجود در تابعی دیگر دسترسی دارد. در واقع در مثال قبلی تابع myFunc یک بستار است. زیرا به متغیرهای محلی تابع outer (و همچنین تابع inner) دسترسی دارد.
نکتهی جالب در مورد بستارها این است که میتوانند به متغیرهای محلی تابعی دسترسی داشته باشند که در حال اجرا نیست. همانطور که در فصل چهارم دیدیم، متغیرهای محلی یک تابع، فقط در زمان اجرای آن تابع وجود دارند. و پس از پایان اجرای تابع، متغیرهای محلی نیز از بین خواهند رفت.
اما همانطور که در مثال فوق دیدید، متغیر myFunc به تابع inner اشاره میکرد. پس با اجرای دستور زیر، تنها تابع inner فراخوانی میشود. اما با این حال تابع inner به متغیر outside که در تابع outer تعریف شده است نیز دسترسی دارد.
myFunc();
← "Inside"
← "Outside"
این ویژگی جالب، شاید در ابتدا بی اهمیت به نظر برسد. اما با استفاده از آن میتوان کارهای جالبی را انجام داد. و بعضاً عدم آگاهی برنامه نویس از این ویژگی، ممکن است منجر به بروز انواع خطاهای منطقی در برنامه شود. مثال زیر نمونهی سادهای از کاربرد بستارها را نشان میدهد. در این مثال تابعی به نام makeAdder با یک پارامتر ورودی به نام x تعریف شده است. تابع makeAdder یک تابع بینام را به عنوان نتیجه بازمیگرداند. این تابع بینام نیز یک پارامتر ورودی به نام y دارد و مقدار بازگشتی از این تابع مجموع اعداد x و y است.
function makeAdder(x) {
return function(y) {
return x + y;
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2));
← 7
console.log(add10(2));
← 12
مشاهده میکنید که در خطوط 5 و 6، با استفاده از تابع makeAdder دو بستار با نامهای add5 و add10 ایجاد میشوند. این بستارها به ترتیب با ارسال اعداد 5 و 10 به تابع makeAdder ایجاد میشوند. در نتیجه add5 تابعی خواهد بود که آرگومان ورودیاش را با 5 (مقدار ذخیره شده در x) جمع میکند. اما add10 تابعی است که آرگومان ورودیاش را با 10 جمع میکند. نتیجهی فراخوانی این بستارها را در خطوط 9 و 11 مشاهده میکنید. این مثال را میتوانید اینجا در CodePen اجرا کنید.
تغییر مقدار متغیرهای محلی
یکی دیگر از ویژگیهای جالب بستارها این است که علاوه بر دسترسی به مقدار متغیرهای محلی سایر توابع، میتوانند مقدار این متغیرها را نیز تغییر دهند. با استفاده از این ویژگی میتوان کارهای مفیدتری را با استفاده از بستارها انجام داد. مثال زیر نحوهی ایجاد یک تابع شمارنده با استفاده از بستارها را نشان میدهد. منظور از تابع شمارنده، تابعی است که مقدار بازگشتی از آن متناسب با تعداد فراخوانیهای آن است.
function makeCounter(start){
let i = start;
return function() {
return i++;
}
}
در این مثال با فراخوانی تابع makeCounter و ارسال یک مقدار اولیه به این تابع، یک تابع بینام بازگردانده میشود که به متغیر i در تابع makeCounter دسترسی دارد. با ذخیرهسازی خروجی تابع makeCounter یک بستار ایجاد میشود که با هر بار فراخوانی آن یک واحد به مقدار قبلی i اضافه شده و بازگردانده میشوند. توجه کنید که با استفاده از تابع makeCounter میتوان در یک برنامه به تعداد دلخواه بستار ایجاد کرد. و برای هر بستار میتوان مقدار اولیهی متفاوتی را جهت شمارش در نظر گرفت. مثلاً در قطعه کد زیر دو بستار با نامهای counter1 و counter2 ایجاد میشوند که مقدار اولیهی متفاوتی دارند.
const counter1 = makeCounter(1);
const counter2 = makeCounter(10);
console.log(counter1());
← 1
console.log(counter1());
← 2
console.log(counter1());
← 3
console.log(counter2());
← 10
console.log(counter2());
← 11
ملاحظه میکنید که مقدار متغیر محلی i برای هر بستار متفاوت است و به مقدار ارسال شده به تابع makeCounter در زمان ایجاد بستار بستگی دارد. این مثال را نیز میتوانید اینجا اجرا کنید.