۵. Closure در عمل؛ ساخت فیلتر قابل استفادهٔ مجدد و وضعیت مستقل
فرض کنید در فروشگاه، چند بخش باید کالاهای «موجود و ارزانتر از بودجه» را نشان دهند. تکرار منطق شرط در هر بخش، تغییر قواعد را دشوار میکند. میتوانیم تابعی بسازیم که یک بار تنظیم شود و سپس ردیفهای مختلف را بررسی کند. این کاربرد مشخص، نقطهٔ شروع خوبی برای فهم Closure است؛ نیازی نیست ابتدا چندین مثال معمایی حفظ کنیم.
Closure یعنی تابع به محیط واژگانی پیرامون محل تعریف خود دسترسی دارد، حتی اگر اجرای تابع بیرونی تمام شده باشد. تابع به متغیرهای آن محیط دسترسی دارد؛ این رفتار را با گرفتن یک عکس ثابت از همهٔ مقدارها اشتباه نگیرید. در مثالهای این درس، همهٔ کدها را مستقل در Node.js یا کنسول مرورگر اجرا کنید.
کارخانهٔ فیلتر محصول
function makeProductFilter(maxPrice, onlyAvailable = true) {
if (!Number.isSafeInteger(maxPrice) || maxPrice < 0) {
throw new RangeError('Invalid budget');
}
return product => product.price <= maxPrice &&
(!onlyAvailable || product.stock > 0);
}
const catalog = [
{ id: 'p1', price: 80000, stock: 2 },
{ id: 'p2', price: 120000, stock: 3 },
{ id: 'p3', price: 60000, stock: 0 }
];
const withinBudget = makeProductFilter(100000);
console.log(catalog.filter(withinBudget).map(item => item.id)); // ['p1']
console.log(catalog.filter(makeProductFilter(100000, false))
.map(item => item.id)); // ['p1', 'p3']تابع بیرونی فقط یک بار تنظیمات فیلتر را میگیرد. تابع برگشتی برای هر محصول اجرا میشود و هنوز به maxPrice و onlyAvailable دسترسی دارد. هر بار فراخوانی کارخانه میتواند تنظیمات مستقل داشته باشد. این مثال، معتبر بودن price و stock محصول را فرض میکند؛ اعتبارسنجی کاتالوگ باید در مرز ورود انجام شود، نه اینکه یک فیلتر نمایشی آن را بیخبر اصلاح کند.
در این طراحی، تغییر بودجهٔ فرم باید به ساختن فیلتر جدید منجر شود. این تصمیم ساده اجازه میدهد از روی ورودی تابع، رفتار را پیشبینی کنیم. اگر قرار است فیلتر زنده و قابل تغییر باشد، آن رفتار را در رابط تابع صریح کنید؛ مخفیانه خواندن متغیری جهانی، آزمون را دشوار میکند.
وضعیت خصوصی کوچک برای پیشنویس
Closure برای نگهداری وضعیت محدود نیز مناسب است. در مثال زیر، هر پیشنویس تیکت وضعیت خودش را دارد و شیء برگشتی راههای مشخص تغییر آن را ارائه میدهد:
function createDraft(initialTitle = '') {
let title = initialTitle;
return {
read() { return title; },
update(next) {
if (typeof next !== 'string' || next.length > 100) {
throw new Error('Invalid draft title');
}
title = next;
},
clear() { title = ''; }
};
}
const first = createDraft('خطای نصب');
const second = createDraft('پرسش دوره');
first.update('خطای اجرای محلی');
console.log(first.read()); // خطای اجرای محلی
console.log(second.read()); // پرسش دوره
first.clear();
console.log(first.read()); // رشتهٔ خالیاین مثال الگوی مستقل آموزشی است و ادعا نمیکند که پروژهٔ تیکت دقیقاً همین کارخانه را استفاده کرده است. چون خروجی read یک متن است، نمیتوان از طریق آن شیء داخلی را تغییر داد. اگر بهجای متن، آرایه یا شیء داخلی برگردانید، دوباره باید دربارهٔ مرجع و کپی درس دوم تصمیم بگیرید. خصوصیبودن متغیر با این روش، مجوز دسترسی یا امنیت سرور ایجاد نمیکند؛ کاربر هنوز کد مرورگر خود را کنترل میکند.
دام برداشت ثابت از متغیر
let category = 'آموزش';
const matches = row => row.category === category;
category = 'خوراک';
console.log(matches({ category: 'آموزش' })); // false
console.log(matches({ category: 'خوراک' })); // trueاین تابع هنگام فراخوانی مقدار فعلی متغیر در دسترس را میخواند. اگر میخواهید تنظیم مستقل داشته باشید، مقدار را به کارخانه بدهید و پارامتر را بعداً تغییر ندهید. همین تفاوت در رویدادها و درخواستهای غیرهمزمان اهمیت پیدا میکند: گاهی باید آخرین وضعیت خوانده شود، گاهی هم یک عملیات باید با تنظیمات لحظهٔ شروع خود ادامه یابد. تصمیم را بر اساس رفتار مطلوب بگیرید.
در برنامههای رابط، عمر Closure نیز اهمیت دارد. یک listener ثبتشده میتواند متغیرهایی را قابل دسترس نگه دارد. وقتی بخش رابط حذف شد، شنوندهٔ غیرلازم را پاک کنید و مرجع آن را نگه ندارید. البته وجود Closure بهتنهایی به معنی نشت حافظه نیست؛ باید مسیرهای زندهٔ نگهداری داده را بررسی کرد. برای این درس، کافی است به هر ثبت رویداد یک مسئولیت پاکسازی نسبت دهید.
پیش از انتخاب وضعیت پنهان، از خود بپرسید آیا همان مسئله با تابعی که همهٔ ورودیها را صریح میگیرد سادهتر حل میشود. اگر پاسخ مثبت است، Closure اجباری نیست. کاربرد مناسب آن در این درس، تنظیم یک رفتار و ساخت نمونههای مستقل است. در بازبینی، نام تابع باید همین قصد را نشان دهد؛ نامهایی مانند makeProductFilter از نام مبهم helper اطلاعات بیشتری دربارهٔ زمان ساخت و زمان استفاده میدهند.
تمرین، پاسخ و معیار تحویل
کارخانهای به نام makeExpenseFilter بنویسید که دسته و حداقل مبلغ را بگیرد و تابع قابل استفاده در filter برگرداند. دستهٔ خالی به معنی همهٔ دستهها باشد. دو فیلتر مستقل بسازید و ثابت کنید تغییر انتخاب فرم با ساخت فیلتر جدید، فیلتر قبلی را عوض نمیکند.
پاسخ راهنما: تنظیمات را بهصورت متن و عدد معتبر دریافت کنید و شرط دسته و مبلغ را با && ترکیب کنید. برای دستهٔ اختیاری از شرط «خالی است یا برابر است» استفاده کنید. پاسخ شما باید ردیف زیر حداقل، مبلغ دقیقاً برابر حداقل، دستهٔ متفاوت و ورودی تنظیمات نامعتبر را پوشش دهد. معیار تحویل، حذف تکرار منطق و توضیح عمر تنظیمات است؛ استفاده از تابع تو در تو بدون دلیل، بهخودیخود امتیاز طراحی محسوب نمیشود.
منابع رسمی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
تألیف و مثالهای اختصاصی: تحریریه درسآوند.