اكتشف كيف تعمل Closures في JavaScript خلف الكواليس، ولماذا هي أقوى من مجرد حفظ المتغيرات. أمثلة حقيقية من الإنتاج تكشف الفخاخ والأسرار التي لا يخبرك بها الدروس التقليدية.
في أحد الأيام، كان السيرفر يعلق كل ساعة دون سبب واضح. بعد ساعات من التنقيب في الأكواد، وجدنا أن دالة صغيرة داخل loop كانت تحتفظ بمرجع لمتغير خارجي دون قصد. النتيجة؟ ٥٠٠ طلب متزامن وكل واحد منهم يحجز مساحة في الذاكرة. المشكلة لم تكن في المنطق، بل في شيء اسمه Closure. إذا كنت تعتقد أنك تعرف الـ Closures لأنك حفظت المثال الكلاسيكي عن counter، فأنت على وشك اكتشاف جانب آخر مظلم منها.
الـ Closures ليست مجرد ميزة لغوية جميلة في JavaScript، بل هي آلية عمل أساسية تتحكم في كيفية تعامل المحرك مع الذاكرة والبيانات. في هذا المقال، لن نتحدث عن التعريف الأكاديمي، بل سنفكك كيف تعمل الـ Closures على مستوى الـ Execution Context، وكيف يمكن أن تكون سلاحاً ذا حدين: إما تبني بها كوداً ذكياً ومرناً، أو تخلق بها مشاكل أداء لا تُحل بسهولة. سنبدأ من الصفر، لكن سننتهي عند مستوى يسمح لك بفهم الكود الذي يكتبه مهندسو Meta وNetflix خلف مكتباتهم الشهيرة.
الـ Closure ليست مجرد دالة داخل دالة تحتفظ بمتغير خارجي. هذه هي النتيجة، لكن الآلية الحقيقية أعمق بكثير. عندما تنشئ دالة داخل دالة أخرى في JavaScript، فإن المحرك لا يحفظ فقط المتغيرات الخارجية التي تستخدمها الدالة الداخلية، بل يحفظ كامل الـ Lexical Environment الذي كانت الدالة الداخلية موجودة فيه عند إنشائها. هذا يعني أن الدالة الداخلية تحتفظ بمرجع لكل ما كان متاحاً في نطاق الدالة الخارجية لحظة إنشائها، حتى لو انتهت الدالة الخارجية وعادت إلى مكدس الاستدعاءات.
لنأخذ مثالاً بسيطاً لكنه يكشف التفاصيل المخفية. في الكود التالي، الدالة `createCounter` تُرجع دالة داخلية `increment` تستخدم المتغير `count` من النطاق الخارجي. لكن ماذا يحدث بالضبط خلف الكواليس؟
function createCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2عندما تُستدعى `createCounter`، ينشئ المحرك سياق تنفيذ جديد (Execution Context) ويحجز مساحة في الذاكرة للمتغير `count`. عند إرجاع الدالة `increment`، لا يتم إتلاف سياق التنفيذ الخارجي فوراً، بل يبقى محفوظاً في الذاكرة طالما أن الدالة `increment` ما زالت موجودة. هذا هو الـ Closure: الدالة الداخلية تحتفظ بمرجع لبيئتها الخارجية حتى بعد انتهاء تنفيذ الدالة الخارجية. إذا لم تكن الدالة الداخلية موجودة، كان المحرك سيحذف المتغير `count` بعد انتهاء `createCounter`، لكن وجود الـ Closure يمنع ذلك.
لفهم تأثير الـ Closures على الذاكرة، يجب أن نعرف كيف يتعامل محرك JavaScript مع الـ Memory Management. عندما تُنشئ دالة داخلية تستخدم متغيرات خارجية، ينشئ المحرك ما يسمى بـ Closure Object في الـ Heap. هذا الكائن يحتفظ بمرجع لكل المتغيرات الخارجية التي تستخدمها الدالة الداخلية، بالإضافة إلى مرجع للنطاق الخارجي بالكامل. هذا يعني أن كل مرة تنشئ فيها دالة داخلية، فإنك تحتفظ بجزء من الذاكرة لا يمكن للمجمع القمامة (Garbage Collector) مسحه طالما أن الدالة الداخلية موجودة في مكان ما في الكود.
لنرى مثالاً أكثر تعقيداً يكشف كيف يمكن للـ Closures أن تسبب تسربات في الذاكرة. في الكود التالي، نقوم بإنشاء مصفوفة من الدوال، كل واحدة منها تحتفظ بمرجع للمتغير `i` من الـ loop الخارجي:
function createFunctions() {
const functi [];
for (var i = 0; i < 5; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const funcs = createFunctions();
funcs[0](); // 5
funcs[1](); // 5
funcs[2](); // 5الجميع يتوقع أن يطبع هذا الكود الأرقام من ٠ إلى ٤، لكن النتيجة هي ٥ خمس مرات. السبب؟ الـ Closure تحتفظ بمرجع للمتغير `i` نفسه، وليس بقيمته في لحظة إنشاء الدالة. عندما ينتهي الـ loop، يكون قيمة `i` تساوي ٥، وكل الدوال الداخلية تشير إلى نفس المتغير. الحل؟ استخدام `let` بدلاً من `var` لأن `let` ينشئ نطاقاً جديداً لكل تكرار في الـ loop، أو استخدام IIFE لحبس قيمة `i` في نطاق جديد:
for (var i = 0; i < 5; i++) {
functions.push((function(value) {
return function() {
console.log(value);
};
})(i));
}هذا المثال يكشف نقطة مهمة: الـ Closures ليست مجرد أداة لكتابة كود أنيق، بل هي آلية تؤثر بشكل مباشر على كيفية تعامل المحرك مع الذاكرة. إذا لم تفهم كيف تعمل، يمكنك بسهولة إنشاء تسربات في الذاكرة تؤدي إلى بطء في التطبيق أو حتى توقف السيرفر تحت ضغط الطلبات.
الـ Closures ليست مجرد مفهوم أكاديمي، بل هي أداة أساسية في مكتبات وأطر العمل التي نستخدمها يومياً. لنأخذ مثالاً من مكتبة React الشهيرة. عندما تستخدم الـ Hook `useState`، فأنت في الحقيقة تستخدم الـ Closure لحفظ حالة المكون بين عمليات الـ Re-render. الدالة التي تُرجعها `useState` تحتفظ بمرجع للمتغير الداخلي الذي يحفظ الحالة، وهذا هو السبب في أنك تستطيع تغيير الحالة دون فقدانها عند إعادة رسم المكون.
function useState(initialValue) {
let state = initialValue;
function setState(newValue) {
state = newValue;
render(); // إعادة رسم المكون
}
function getState() {
return state;
}
return [getState, setState];
}هذا الكود المبسط يوضح كيف تستخدم React الـ Closure لحفظ الحالة بين عمليات الـ Re-render. الدالة `getState` تحتفظ بمرجع للمتغير `state` حتى بعد انتهاء تنفيذ `useState`، وهذا هو السبب في أن الحالة تبقى محفوظة. إذا لم تكن الـ Closure موجودة، لكانت الحالة تُفقد بعد كل عملية إعادة رسم.
مثال آخر من مكتبة Redux. عندما تنشئ middleware في Redux، فإنك تستخدم الـ Closure لحفظ مرجع للكائن `store` والدالة `next`. الـ Middleware في Redux هو مجرد دالة تُرجع دالة تُرجع دالة أخرى، وكل واحدة منها تحتفظ بمرجع للنطاق الخارجي:
const loggerMiddleware = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};في هذا المثال، الدالة الداخلية `action => {...}` تحتفظ بمرجع للمتغيرين `store` و`next` من النطاقين الخارجيين. هذا هو السبب في أن الـ Middleware تستطيع الوصول إلى الكائن `store` حتى بعد انتهاء تنفيذ الدالة الخارجية. هذه هي قوة الـ Closures: القدرة على حفظ الحالة والبيانات بين استدعاءات الدوال المختلفة دون الحاجة إلى تمريرها كوسائط.
رغم قوة الـ Closures، إلا أنها يمكن أن تكون مصدراً لمشاكل حقيقية في الإنتاج إذا لم تُستخدم بحذر. أحد أكبر الفخاخ هو تسرب الذاكرة (Memory Leak) بسبب الاحتفاظ بمراجع لمتغيرات كبيرة دون داعٍ. لنفترض أنك تنشئ دالة داخلية تحتفظ بمرجع لمصفوفة كبيرة داخل دالة خارجية:
function processData() {
const largeArray = new Array(1000000).fill('data');
return function() {
console.log(largeArray.length);
};
}
const leakyFunction = processData();في هذا المثال، الدالة الداخلية تحتفظ بمرجع للمصفوفة `largeArray` حتى لو لم تعد بحاجة إليها. إذا احتفظت بالدالة الداخلية في مكان ما في الكود، فإن المصفوفة الكبيرة ستبقى في الذاكرة ولن يستطيع المجمع القمامة مسحها. الحل؟ إذا كنت تعرف أنك لن تحتاج للمتغير الخارجي بعد انتهاء الدالة الخارجية، يمكنك تعيين قيمته إلى `null` لتحرير الذاكرة:
function processData() {
const largeArray = new Array(1000000).fill('data');
const resultFunction = function() {
console.log(largeArray.length);
};
largeArray = null; // تحرير الذاكرة
return resultFunction;
}فخ آخر شائع هو استخدام الـ Closures داخل loops دون فهم كيفية تعامل المحرك مع المتغيرات. كما رأينا في المثال السابق، استخدام `var` داخل loop مع دالة داخلية يؤدي إلى نتائج غير متوقعة. الحل هو استخدام `let` أو IIFE لحبس قيمة المتغير في نطاق جديد. لكن حتى مع `let`، يجب أن تكون حذراً من الأداء. كل تكرار في loop ينشئ نطاقاً جديداً، وهذا يعني أن المحرك ينشئ سياق تنفيذ جديد لكل تكرار، مما قد يؤثر على الأداء في loops كبيرة.
أخيراً، يجب أن تكون حذراً عند استخدام الـ Closures مع الـ Event Listeners. إذا أنشأت دالة داخلية داخل loop وأضفتها كحدث لـ DOM Element، فإن كل دالة ستحتفظ بمرجع للمتغير الخارجي، مما قد يؤدي إلى تسرب في الذاكرة أو سلوك غير متوقع:
for (var i = 0; i < 5; i++) {
const button = document.createElement('button');
button.textC `Button ${i}`;
button.addEventListener('click', function() {
console.log(i); // سيطبع 5 دائماً
});
document.body.appendChild(button);
}الحل؟ استخدام `let` أو IIFE لحبس قيمة `i` في نطاق جديد لكل تكرار. لكن حتى مع الحل، يجب أن تتذكر أن كل Event Listener تحتفظ بمرجع للدالة الداخلية، مما يعني أن الذاكرة لن تُحرر حتى يتم إزالة الـ Event Listener نفسه. إذا كنت تضيف Event Listeners داخل loop كبير، فقد تواجه مشاكل في الأداء بسبب تراكم الدوال في الذاكرة.
إذا أردت أن تتقن الـ Closures حقاً، فلا يكفي أن تحفظ الأمثلة الكلاسيكية. يجب أن تفهم كيف تعمل خلف الكواليس، وتعرف متى تستخدمها ومتى تتجنبها. إليك بعض النصائح العملية التي تعلمتها من سنوات العمل في الإنتاج:
const obj = {
name: 'Object',
getName: function() {
const self = this;
return function() {
return self.name;
};
}
};
const getName = obj.getName();
console.log(getName()); // 'Object'الـ Closures ليست مجرد أداة لكتابة كود أنيق، بل هي آلية قوية يمكن أن تجعل كودك أكثر مرونة وقوة. لكن مع هذه القوة تأتي مسؤولية فهم كيفية عملها وتأثيرها على الأداء والذاكرة. إذا استخدمت الـ Closures بحكمة، يمكنك بناء تطبيقات سريعة وفعالة. إذا أسأت استخدامها، فقد تواجه مشاكل في الأداء وتسربات في الذاكرة يصعب تتبعها.
الـ Closures هي سلاح ذو حدين: إما تبني بها كوداً ذكياً ومرناً، أو تخلق بها مشاكل أداء لا تُحل بسهولة. القاعدة الذهبية؟ استخدمها عندما تحتاج إلى حفظ حالة بين استدعاءات الدوال، وتجنبها عندما تحتفظ بمراجع لمتغيرات كبيرة دون داعٍ. وإذا وجدت نفسك تكتب دالة داخلية داخل loop، توقف وفكر مرتين: هل هذا هو الحل الأمثل، أم أنني سأخلق تسرباً في الذاكرة؟