closures ليست مجرد ميزة في JavaScript، بل هي سلاح ذو حدين: يمكنها تحسين أداء الكود أو تدميره تماماً. اكتشف كيف تتحكم في ذاكرة الدوال وتحول الارتباك إلى إتقان من خلال أمثلة حقيقية وحالات استخدام عملية.
في أحد المشاريع الكبيرة لشركة تقنية معروفة، كان هناك bug غريب يظهر فقط في بيئة الإنتاج. بعد ساعات من Debugging، اكتشفنا أن المشكلة تكمن في closure لم يتم التعامل معها بشكل صحيح داخل loop، مما تسبب في تسرب ذاكرة (memory leak) أدى إلى تجميد السيرفر بعد ساعات من التشغيل. لم يكن الخطأ في المنطق البرمجي، بل في كيفية تعامل JavaScript مع الذاكرة والدوال المغلقة. هذه هي القوة الخفية لـ closures: يمكنها أن تجعل الكود أنيقاً ومرناً، أو تحول مشروعك إلى كابوس لا ينتهي.
closures في JavaScript هي واحدة من تلك المفاهيم التي تبدو بسيطة في البداية، لكنها سرعان ما تكشف عن تعقيداتها عندما تبدأ في استخدامها في مشاريع حقيقية. ليست مجرد دالة داخل دالة، بل هي علاقة معقدة بين الدوال والبيئة التي نشأت فيها. عندما تفهمclosures بعمق، ستتمكن من كتابة كود أكثر كفاءة وأقل عرضة للأخطاء، وستفهم لماذا بعض المكتبات الشهيرة مثل React وRedux تعتمد عليها بشكل أساسي.
عندما تسمع تعريف closures كـ "دالة داخل دالة"، فهذا تبسيط مفرط قد يقودك إلى فهم خاطئ. في الواقع، closure هي دالة تحتفظ بمرجع إلى المتغيرات الموجودة في نطاقها الخارجي (scope) حتى بعد انتهاء تنفيذ ذلك النطاق. هذا يعني أن الدالة "تتذكر" البيئة التي نشأت فيها، حتى لو كانت تلك البيئة قد انتهت بالفعل. هذا السلوك ليس مجرد ميزة لغوية، بل هو نتيجة مباشرة لكيفية تعامل JavaScript مع الذاكرة وتنفيذ الكود.
لفهم هذا بشكل عملي، تخيل أنك تعمل في مطعم وتكتب طلبية لعملاء. عندما تنتهي من كتابة الطلب، تتوقع أن تختفي الورقة التي كتبت عليها الطلب بعد تسليمها للطاهي. لكن في عالم closures، الورقة تبقى موجودة في ذاكرتك حتى بعد أن تنتهي من كتابة الطلب، ويمكنك الرجوع إليها في أي وقت. هذا بالضبط ما تفعله closures: تحتفظ بالدوال بمتغيرات كانت موجودة في النطاق الخارجي حتى بعد انتهاء تنفيذ ذلك النطاق.
// مثال بسيط يوضح مفهوم Closure
function createCounter() {
let count = 0; // متغير في النطاق الخارجي
return function() {
count++; // الدالة الداخلية تحتفظ بمرجع لـ count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// حتى بعد انتهاء تنفيذ createCounter، الدالة الداخلية تحتفظ بـ countفي المثال السابق، عندما ننشئ counter باستخدام createCounter، الدالة الداخلية تحتفظ بمرجع إلى المتغير count حتى بعد انتهاء تنفيذ createCounter. هذا هو جوهر closures: الدوال تحتفظ بمرجع إلى المتغيرات في نطاقها الخارجي حتى بعد انتهاء ذلك النطاق. لكن لماذا يحدث هذا؟ وكيف يتعامل محرك JavaScript مع هذه الذاكرة؟
لفهم كيفية عمل closures بعمق، يجب أن نتعمق قليلاً في كيفية تعامل محرك JavaScript مع الذاكرة والتنفيذ. عندما تنشئ دالة في JavaScript، يتم إنشاء شيئين: الكائن الوظيفي (function object) والبيئة المعجمية (lexical environment) التي تحتوي على المتغيرات المحلية والدوال الموجودة في النطاق الحالي. عندما تنشئ closure، يتم إنشاء مرجع من الدالة الداخلية إلى البيئة المعجمية للنطاق الخارجي، وهذا المرجع يبقى حتى بعد انتهاء تنفيذ النطاق الخارجي.
لنفترض أنك تعمل على مشروع يتطلب إنشاء عدة دوال تتعامل مع نفس البيانات. بدلاً من تمرير البيانات لكل دالة على حدة، يمكنك استخدام closures لإنشاء بيئة مشتركة بين هذه الدوال. هذا ليس مجرد توفير للكتابة، بل هو تحسين في الأداء لأنك تقلل من تكرار البيانات في الذاكرة. لكن احذر: هذا الأسلوب يمكن أن يؤدي إلى تسرب الذاكرة إذا لم تتعامل معه بحذر، خاصة في التطبيقات الكبيرة التي تعمل لفترات طويلة.
// مثال يوضح كيفية استخدام Closures لإنشاء بيئة مشتركة
function createBankAccount(initialBalance) {
let balance = initialBalance;
let transacti [];
return {
deposit: function(amount) {
balance += amount;
transactions.push({ type: 'deposit', amount });
return balance;
},
withdraw: function(amount) {
if (amount > balance) {
throw new Error('Insufficient funds');
}
balance -= amount;
transactions.push({ type: 'withdraw', amount });
return balance;
},
getBalance: function() {
return balance;
},
getTransactions: function() {
return [...transactions]; // نسخ المصفوفة لتجنب التعديل الخارجي
}
};
}
const account = createBankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.getTransactions()); // [{ type: 'deposit', amount: 500 }, { type: 'withdraw', amount: 200 }]في المثال السابق، استخدمنا closures لإنشاء بيئة مشتركة بين عدة دوال (deposit، withdraw، getBalance، getTransactions). كل هذه الدوال تحتفظ بمرجع إلى المتغيرات balance وtransactions الموجودة في النطاق الخارجي. هذا الأسلوب شائع جداً في البرمجة الوظيفية ويستخدم في العديد من المكتبات الشهيرة مثل Redux لإدارة الحالة (state management). لكن لاحظ كيف قمنا بنسخ المصفوفة transactions عند إرجاعها لمنع التعديل الخارجي، هذه نقطة مهمة جداً لمنع تسرب البيانات.
closures ليست مجرد مفهوم نظري، بل هي أداة أساسية في العديد من المكتبات والأطر الشهيرة. لنأخذ React كمثال: عندما تستخدم hooks مثل useState أو useEffect، فأنت في الواقع تستخدم closures تحت الغطاء. الدوال التي تمررها لهذه الـ hooks تحتفظ بمرجع إلى المتغيرات في النطاق الخارجي، وهذا ما يسمح لـ React بإدارة الحالة والتأثيرات الجانبية بشكل فعال.
في مشروع حقيقي عملت عليه، استخدمنا closures لإنشاء نظام تحكم في الوصول (access control) ديناميكي. بدلاً من التحقق من الصلاحيات في كل دالة، أنشأنا closure تحتفظ بمرجع إلى صلاحيات المستخدم الحالية، مما قلل من تكرار الكود وتحسين الأداء بشكل ملحوظ. لكن كان علينا أن نكون حذرين جداً مع الـ garbage collection، لأن closures يمكن أن تمنع المتغيرات من أن يتم جمعها إذا لم نتعامل معها بحذر.
// مثال على استخدام Closures في نظام تحكم في الوصول
function createAccessController(userPermissions) {
const permissi new Set(userPermissions); // استخدام Set لتحسين الأداء
return {
hasAccess: function(resource) {
return permissions.has(resource);
},
grantAccess: function(resource) {
permissions.add(resource);
},
revokeAccess: function(resource) {
permissions.delete(resource);
},
getPermissions: function() {
return Array.from(permissions);
}
};
}
const adminController = createAccessController(['users', 'settings', 'reports']);
console.log(adminController.hasAccess('users')); // true
console.log(adminController.hasAccess('billing')); // false
adminController.grantAccess('billing');
console.log(adminController.hasAccess('billing')); // trueإذا كنت قد استخدمت Redux، فأنت بالفعل تستخدم closures دون أن تدرك ذلك. عندما تنشئ reducer في Redux، فإن الدالة التي تمررها كـ reducer تحتفظ بمرجع إلى الحالة الحالية (current state) والبيانات التي تمررها لـ action. هذا يسمح لـ Redux بإدارة الحالة بشكل فعال وتنفيذ التحديثات بشكل متوقع. لكن هذا أيضاً يعني أنك يجب أن تكون حذراً جداً عند التعامل مع البيانات في reducers، لأن أي تعديل غير مقصود يمكن أن يؤدي إلى سلوك غير متوقع بسبب طبيعة closures.
// مثال مبسط على كيفية استخدام Redux لـ Closures
function createStore(reducer, initialState) {
let currentState = initialState;
return {
getState: function() {
return currentState;
},
dispatch: function(action) {
currentState = reducer(currentState, action); // reducer تحتفظ بمرجع لـ currentState
return action;
}
};
}
function counterReducer(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
const store = createStore(counterReducer, 0);
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // 1
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // 2closures يمكن أن تكون سلاحاً قوياً، لكنها أيضاً يمكن أن تكون مصدراً لأخطاء صعبة التشخيص. أحد أكثر الأخطاء شيوعاً هو مشكلة الـ loop مع closures، حيث تحاول إنشاء عدة دوال داخل loop، وكلها تحتفظ بمرجع إلى نفس المتغير. هذا غالباً ما يؤدي إلى سلوك غير متوقع لأن جميع الدوال ستشير إلى القيمة النهائية للمتغير، وليس القيمة التي كان عليها عند إنشاء الدالة.
في أحد المشاريع التي عملت عليها، واجهنا مشكلة غريبة حيث كانت جميع الأحداث في واجهة المستخدم تشير إلى نفس العنصر، رغم أننا أنشأنا مستمعين للأحداث لكل عنصر على حدة. بعد ساعات من Debugging، اكتشفنا أن المشكلة كانت في closures داخل loop. كان كل مستمع للأحداث يحتفظ بمرجع إلى نفس المتغير، مما أدى إلى سلوك غير متوقع. الحل كان بسيطاً: استخدام let بدلاً من var في loop، أو إنشاء scope جديد لكل تكرار باستخدام IIFE (Immediately Invoked Function Expression).
// مشكلة الـ Loop مع Closures - مثال خاطئ
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // سيطبع 3 ثلاث مرات، وليس 0, 1, 2
}, 1000);
}
// الحل باستخدام let
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // سيطبع 0, 1, 2
}, 1000);
}
// الحل باستخدام IIFE
for (var k = 0; k < 3; k++) {
(function(k) {
setTimeout(function() {
console.log(k); // سيطبع 0, 1, 2
}, 1000);
})(k);
}closures يمكن أن تؤدي إلى تسرب الذاكرة (memory leaks) إذا لم يتم التعامل معها بحذر. عندما تحتفظ دالة بمرجع إلى متغيرات في نطاقها الخارجي، فإن تلك المتغيرات لا يمكن جمعها بواسطة الـ garbage collector حتى يتم التخلص من الدالة نفسها. في التطبيقات الكبيرة التي تعمل لفترات طويلة، مثل تطبيقات Node.js أو تطبيقات الويب التي تبقى مفتوحة لساعات، يمكن أن يؤدي هذا إلى زيادة مستمرة في استخدام الذاكرة حتى يتم تجميد التطبيق.
في أحد مشاريع Node.js التي عملت عليها، كان لدينا خدمة تعمل في الخلفية وتتعامل مع آلاف الطلبات في الدقيقة. بعد ساعات من التشغيل، لاحظنا أن استخدام الذاكرة يتزايد بشكل مستمر حتى يتم إعادة تشغيل الخدمة تلقائياً. بعد التحقيق، اكتشفنا أن المشكلة كانت في closures تحتفظ بمرجع إلى بيانات كبيرة داخل دوال يتم استدعاؤها بشكل متكرر. الحل كان بسيطاً: التخلص من المراجع عندما لا تكون هناك حاجة إليها، أو استخدام WeakMap بدلاً من closures في بعض الحالات.
// مثال على تسرب الذاكرة مع Closures
function createMemoryLeak() {
const bigData = new Array(1000000).fill('data'); // بيانات كبيرة
return function() {
console.log(bigData.length); // تحتفظ بمرجع لـ bigData
};
}
// هذا سيؤدي إلى تسرب الذاكرة إذا تم استدعاؤه بشكل متكرر
const leakyFunction = createMemoryLeak();
// الحل: التخلص من المرجع عندما لا يكون هناك حاجة إليه
function createSafeFunction() {
const bigData = new Array(1000000).fill('data');
return function() {
console.log(bigData.length);
// بعد الاستخدام، يمكننا تعيين bigData إلى null لتسهيل جمعها
// لكن هذا يتطلب إدارة يدوية، وهو ليس مثالياً
};
}closures هي أداة قوية، لكن مثل أي أداة قوية، يجب استخدامها بحذر. هناك بعض أفضل الممارسات التي يجب اتباعها لتجنب المشاكل الشائعة وضمان أن كودك يعمل بكفاءة. أولاً، تجنب استخدام closures في loops إلا إذا كنت متأكداً من أنك تفهم تماماً كيف ستتصرف. استخدم let بدلاً من var في loops لتجنب مشاكل النطاق، أو استخدم IIFE لإنشاء scope جديد لكل تكرار.
ثانياً، كن حذراً مع تسرب الذاكرة. إذا كنت تحتفظ بمرجع إلى بيانات كبيرة داخل closure، فكر في كيفية التخلص من هذا المرجع عندما لا تكون هناك حاجة إليه. في بعض الحالات، قد يكون من الأفضل استخدام WeakMap بدلاً من closures لتجنب الاحتفاظ بالمراجع بشكل دائم. ثالثاً، استخدم closures لإنشاء واجهات برمجية نظيفة ومغلفة. هذا هو المكان الذي تتألق فيه closures، حيث يمكنك إنشاء دوال تتعامل مع البيانات الداخلية دون تعريض تلك البيانات للخارج.
// مثال على استخدام WeakMap بدلاً من Closures لتجنب تسرب الذاكرة
const weakMap = new WeakMap();
function createSafeCounter() {
const data = { count: 0 };
weakMap.set(data, {}); // تخزين بيانات مؤقتة
return {
increment: function() {
data.count++;
return data.count;
},
getCount: function() {
return data.count;
},
// يمكن جمع data بواسطة garbage collector عندما لا يكون هناك مراجع خارجية
};
}
const counter = createSafeCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2closures في JavaScript هي واحدة من أقوى الأدوات التي يمكنك استخدامها لكتابة كود أنيق ومرن، لكنها أيضاً يمكن أن تكون مصدراً لأخطاء صعبة التشخيص وتسرب الذاكرة. المفتاح هو فهم كيفية عملها بعمق، ومعرفة متى تستخدمها ومتى تتجنبها. عندما تتقن closures، ستتمكن من كتابة كود أكثر كفاءة وأقل عرضة للأخطاء، وستفهم لماذا بعض المكتبات الشهيرة تعتمد عليها بشكل أساسي.
في النهاية، closures ليست مجرد ميزة لغوية، بل هي نمط تفكير. عندما تبدأ في التفكير في كيفية احتفاظ الدوال بالبيئة التي نشأت فيها، ستجد نفسك تكتب كوداً أكثر قوة ومرونة. لكن تذكر دائماً: مع القوة تأتي المسؤولية. استخدم closures بحكمة، وتأكد من أنك تفهم تماماً كيف ستتصرف في بيئتك البرمجية، خاصة في التطبيقات الكبيرة التي تعمل لفترات طويلة.
closures هي مثل الذكريات في عقلك: تحتفظ بالمعلومات حتى عندما لا تكون بحاجة إليها، وأحياناً تتذكر أشياء لا تريد تذكرها.
— مهندس برمجيات مجهول