هل تساءلت يوماً لماذا يحتفظ الكود بقيم متغيرات لم تعد موجودة؟ Closures ليست مجرد ميزة في JavaScript، بل هي سلاح ذو حدين: يمكنها تحسين أداء التطبيقات أو تدميرها عبر Memory Leaks. سنفككها من الصفر إلى الاحتراف بأمثلة حقيقية من عالم الإنتاج.
في أحد المشاريع الكبيرة لشركة ناشئة في دبي، كان السيرفر ينهار كل ساعتين دون سبب واضح. بعد 12 ساعة من Debugging، اكتشفنا أن أحد المطورين استخدم Closure داخل loop لإنشاء Event Listeners لعشرات الآلاف من العناصر في DOM. النتيجة؟ كل listener احتفظ بمرجع لـ i القديمة، مما أدى إلى تسريب 2 جيجابايت من الذاكرة في غضون دقائق. Closures ليست مجرد مفهوم نظري تُشرح في الدروس؛ إنها واقع يومي قد يكلفك وظيفتك إذا لم تفهمها جيداً.
المشكلة الأكبر أن معظم الشروحات تتوقف عند المثال الكلاسيكي: دالة داخل دالة تحتفظ بمتغير خارجي. لكن الحقيقة أن Closures تتسلل إلى كل ركن في من الـ Event Handlers إلى الـ Modules، ومن الـ Promises إلى الـ Generators. إذا كنت تريد كتابة كود نظيف وسريع، عليك أن تفهم بالضبط كيف تعمل الذاكرة هنا، ولماذا أحياناً تُحبس المتغيرات في سجن لا تستطيع الهروب منه حتى بعد انتهاء تنفيذ الدالة.
عندما تعلن متغيراً باستخدام let أو const داخل دالة، فإن JavaScript تخصص له مكاناً في ذاكرة الـ Execution Context الخاص بهذه الدالة. عند انتهاء تنفيذ الدالة، يُفترض أن تُحذف هذه المتغيرات، أليس كذلك؟ هنا يأتي دور الـ Garbage Collector، الذي يمسح كل ما ليس له مراجع نشطة. لكن المشكلة تبدأ عندما تحتفظ دالة داخلية بمرجع لمتغير خارجي بعد انتهاء الدالة الأم. هذا المرجع يمنع الـ Garbage Collector من مسح المتغير، مما يؤدي إلى ما يُسمى بـ Memory Leak.
لنأخذ مثالاً بسيطاً لكن خطيراً: تخيل أنك تنشئ 10,000 زر في صفحة ويب، وكل زر لديه Event Listener يستخدم متغيراً من loop. إذا استخدمت var بدلاً من let، فكل listener سيشير إلى نفس المتغير i، الذي سيصبح 10,000 عند انتهاء الـ loop. النتيجة؟ كل زر عند الضغط عليه سينفذ الكود بنفس القيمة، وهذا ليس ما تريده بالتأكيد. هذا هو بالضبط ما حدث في المشروع الذي ذكرته في المقدمة، لكن مع Closures بدلاً من var.
// مثال على Memory Leak باستخدام Closure داخل loop
for (var i = 0; i < 10000; i++) {
var button = document.createElement('button');
button.textC 'Button ' + i;
button.addEventListener('click', function() {
console.log('Clicked button:', i); // جميع الأزرار ستطبع 10000!
});
document.body.appendChild(button);
}
// الحل باستخدام let و Closure صحيحة
for (let j = 0; j < 10000; j++) {
let button = document.createElement('button');
button.textContent = 'Button ' + j;
button.addEventListener('click', function() {
console.log('Clicked button:', j); // كل زر سيطبع رقمه الصحيح
});
document.body.appendChild(button);
}عندما تُنشئ دالة داخل دالة أخرى، فإن الدالة الداخلية تحتفظ بمرجع لكل المتغيرات الموجودة في نطاق الدالة الخارجية، حتى بعد انتهاء تنفيذ الدالة الخارجية. هذا المرجع ليس مجرد نسخة من القيمة، بل هو مرجع حي يُحدث مع كل تغيير. هذا يعني أنك إذا عدلت متغيراً خارجياً داخل دالة داخلية، فإن التغيير سيظهر في كل مكان يستخدم هذا المتغير عبر Closures مختلفة.
لنعد إلى مثال الـ Event Listeners. عندما تُنشئ listener داخل loop، فإن كل listener يحتفظ بمرجع لمتغير الـ loop (i أو j). إذا استخدمت var، فإن هذا المتغير يُعلن مرة واحدة في نطاق الدالة، وبالتالي كل listener سيشير إلى نفس المتغير الذي يتغير مع كل تكرار. أما إذا استخدمت let، فإن كل تكرار يُنشئ نطاقاً جديداً، وبالتالي كل listener سيحتفظ بقيمة مختلفة من j، وهذا هو السلوك المتوقع.
// مثال يوضح كيف تحتفظ Closure بمرجع حي للمتغير
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// حتى لو حاولنا الوصول إلى count مباشرة، لن نستطيع
console.log(count); // ReferenceError: count is not definedفي هذا المثال، الدوال increment و decrement و getCount تحتفظ جميعاً بمرجع للمتغير count الذي أُعلن داخل createCounter. حتى بعد انتهاء تنفيذ createCounter، تظل هذه الدوال قادرة على الوصول إلى count وتعديله، وهذا هو جوهر الـ Closure. لاحظ أن count غير قابل للوصول إليه من خارج هذه الدوال، مما يوفر مستوى من التشفير والخصوصية في JavaScript.
في عالم الإنتاج، تُستخدم Closures في كل مكان تقريباً. من الـ Modules في Node.js إلى الـ React Hooks، ومن الـ Event Delegation إلى الـ Memoization. لكن استخدامها بدون فهم عميق قد يؤدي إلى كوارث. لنأخذ مثالاً من مكتبة React الشهيرة: عند استخدام useState داخل مكون، فإن React تستخدم Closure لحفظ حالة المكون بين الـ Renders المختلفة. إذا لم تفهم كيف تعمل هذه الـ Closure، فقد تواجه مشكلة الـ Stale Closure، حيث تحتفظ الدالة بقيمة قديمة من الحالة بدلاً من القيمة الحالية.
مثال آخر من Node.js: عند إنشاء Server باستخدام Express، فإن كل Request Handler هو في الواقع Closure تحتفظ بمرجع للمتغيرات المعلنة في نطاق الدالة الخارجية. هذا يسمح لك بالوصول إلى req و res داخل الـ Handler، لكنه أيضاً يعني أنك إذا أنشأت متغيراً داخل الدالة الخارجية واستخدمته داخل Handler، فإن هذا المتغير سيكون مشتركاً بين جميع الـ Requests، مما قد يؤدي إلى تسريبات بيانات بين المستخدمين!
// مثال خطير: تسريب بيانات بين المستخدمين بسبب Closure غير صحيحة
const express = require('express');
const app = express();
let userData; // متغير مشترك بين جميع الـ Requests!
app.get('/user/:id', (req, res) => {
userData = fetchUserData(req.params.id); // تعديل المتغير المشترك
res.json(userData);
});
app.get('/profile', (req, res) => {
res.json(userData); // قد يعرض بيانات مستخدم آخر!
});
app.listen(3000);
// الحل الصحيح: تجنب المتغيرات المشتركة واستخدم locals
app.get('/user/:id', (req, res) => {
const data = fetchUserData(req.params.id);
res.locals.userData = data; // استخدام locals بدلاً من المتغير المشترك
res.json(data);
});في هذا المثال، المتغير userData مشترك بين جميع الـ Requests، مما يعني أنه إذا قام مستخدمان بإرسال طلبات متزامنة، فقد يحصل أحدهما على بيانات الآخر. هذا النوع من الأخطاء شائع جداً في التطبيقات الكبيرة، ويمكن أن يؤدي إلى مشاكل أمنية خطيرة. الحل هو تجنب المتغيرات المشتركة واستخدام locals أو تمرير البيانات مباشرةً بين الدوال.
أحد أكثر المشاكل تعقيداً مع Closures هي ما يُسمى بـ Stale Closure. تحدث هذه المشكلة عندما تحتفظ دالة بمرجع لمتغير خارجي، لكن هذا المتغير يتغير لاحقاً، والدالة لا تُحدث مرجعها. هذا شائع جداً في الـ Event Handlers و الـ Callbacks، خاصةً في مكتبات مثل React حيث تُعاد تهيئة المكونات باستمرار.
لنأخذ مثالاً من React: إذا أنشأت Event Handler داخل useEffect ويعتمد على متغير من الحالة، فإن هذا Handler سيحتفظ بقيمة الحالة عند إنشائه، ولن يُحدثها مع تغير الحالة. هذا يعني أنك إذا ضغطت على زر بعد تغيير الحالة، فقد ترى قيمة قديمة بدلاً من القيمة الحالية. الحل هو استخدام useCallback مع قائمة اعتماديات، لكن حتى هذا قد يكون معقداً إذا لم تفهم كيف تعمل الـ Closures خلف الكواليس.
// مثال على Stale Closure في React
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count); // سيطبع دائماً القيمة الأولية لـ count!
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // قائمة الاعتماديات فارغة = Stale Closure!
return <div>Count: {count}</div>;
}
// الحل الصحيح: استخدام قائمة الاعتماديات الصحيحة
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count);
setCount(c => c + 1); // استخدام الدالة Updater
}, 1000);
return () => clearInterval(timer);
}, [count]); // تحديث الـ Closure مع تغير countفي المثال الأول، الـ Closure داخل setInterval تحتفظ بقيمة count عند إنشائها، وبالتالي ستطبع دائماً القيمة الأولية لـ count (0) بدلاً من القيمة الحالية. الحل هو إما استخدام قائمة الاعتماديات الصحيحة، أو استخدام الدالة Updater داخل setCount التي تستقبل القيمة الحالية كـ parameter. هذا يضمن أنك دائماً تعمل على القيمة الأحدث، وليس على نسخة قديمة محبوسة داخل الـ Closure.
في المشاريع الكبيرة، قد تجد نفسك مضطراً لاستخدام Closures متداخلة، حيث تحتفظ دالة داخلية بمرجع لمتغير من دالة خارجية، وتحتفظ دالة أخرى داخلها بمرجع لمتغير من الدالة الداخلية، وهكذا. هذا قد يؤدي إلى ما يُسمى بـ Closure Hell، حيث يصبح من الصعب تتبع المتغيرات ومعرفة أي دالة تحتفظ بأي مرجع. هذا شائع جداً في الكود القديم الذي يستخدم الـ Callbacks المتداخلة بدلاً من الـ Promises أو async/await.
لنأخذ مثالاً من مكتبة jQuery القديمة، حيث كان يُستخدم الكثير من الـ Callbacks المتداخلة للتعامل مع الـ AJAX Requests. كل callback يحتفظ بمرجع لمتغيرات من الـ Callbacks السابقة، مما يجعل الكود صعب الفهم والصيانة. اليوم، مع استخدام الـ Promises و async/await، أصبح من السهل تجنب هذا النوع من التعقيد، لكن فهم الـ Closures المتداخلة يظل ضرورياً لفهم الكود القديم أو لتصحيح الأخطاء في المكتبات الحديثة.
// مثال على Closures المتداخلة (Callback Hell)
function fetchUserData(userId, callback) {
fetch(`/api/users/${userId}`)
.then(resp> response.json())
.then(user => {
fetch(`/api/posts?userId=${userId}`)
.then(response => response.json())
.then(posts => {
fetch(`/api/comments?userId=${userId}`)
.then(response => response.json())
.then(comments => {
callback({ user, posts, comments }); // كل هذه المتغيرات محبوسة داخل Closures
})
.catch(error => callback(null, error));
})
.catch(error => callback(null, error));
})
.catch(error => callback(null, error));
}
// الحل باستخدام async/await لتجنب Closure Hell
async function fetchUserDataAsync(userId) {
try {
const user = await fetch(`/api/users/${userId}`).then(res => res.json());
const posts = await fetch(`/api/posts?userId=${userId}`).then(res => res.json());
const comments = await fetch(`/api/comments?userId=${userId}`).then(res => res.json());
return { user, posts, comments };
} catch (error) {
throw error;
}
}في المثال الأول، كل callback يحتفظ بمرجع للمتغيرات من الـ Callbacks السابقة، مما يجعل الكود صعب القراءة والفهم. أما في المثال الثاني باستخدام async/await، فإن الكود يصبح مسطحاً وسهل الفهم، ولا توجد حاجة للـ Closures المتداخلة. هذا يوضح كيف أن فهم الـ Closures يمكن أن يساعدك في كتابة كود أفضل وأكثر قابلية للصيانة.
بعد أكثر من عشر سنوات في كتابة JavaScript، إليك نصيحتي الذهبية: استخدم Closures بحذر، ولا تعتمد عليها إلا عندما تكون الحل الوحيد. إذا وجدت نفسك تكتب دالة داخل دالة داخل دالة، توقف وفكر: هل هناك طريقة أبسط؟ في معظم الحالات، يمكنك استبدال الـ Closures المتداخلة بالـ Modules أو الـ Classes أو حتى الـ Promises. وإذا كنت مضطراً لاستخدامها، فتأكد من أنك تفهم تماماً ما تحتفظ به كل دالة، وكيف ستؤثر على الذاكرة والأداء.
من تجربتي، أكبر مشكلة مع Closures ليست في استخدامها، بل في إساءة استخدامها. عندما تحتفظ دالة بمرجع لمتغير كبير أو لمصفوفة ضخمة، فإنك تخاطر بتسريب الذاكرة. وعندما تعتمد على متغير خارجي دون أن تُحدث مرجعك، فإنك تخاطر بالحصول على بيانات قديمة. لذلك، القاعدة الأولى هي: قلل من استخدام المتغيرات الخارجية داخل الـ Closures، وحاول تمرير البيانات كparameters بدلاً من الاعتماد على الـ Scope. القاعدة الثانية: إذا كنت تستخدم Closures مع الـ Event Listeners أو الـ Timeouts، فتأكد من تنظيفها بشكل صحيح لتجنب Memory Leaks.
Closures ليست مجرد ميزة في JavaScript؛ إنها أداة قوية يمكن أن تجعل كودك أنظف وأكثر كفاءة، أو تدمره تماماً إذا أسأت استخدامها. المفتاح هو الفهم العميق: كيف تعمل الذاكرة، وكيف تحتفظ الدوال بمراجع المتغيرات، وكيف يؤثر ذلك على الأداء. إذا أتقنت هذا المفهوم، ستكتب كوداً أفضل، وستتفادى الكثير من الأخطاء التي يقع فيها المطورون الجدد وحتى المتوسطون. ابدأ بتجربة الأمثلة بنفسك، وعدّل فيها، وشاهد كيف يتغير السلوك. هذه هي الطريقة الوحيدة لتعلم الـ Closures حقاً: بالتجربة والخطأ.