Kodokon kodokon.com

الإغلاقات والنطاق المعجمي

أتقن كيف تلتقط الإغلاقات المتغيرات لتغلّف الحالة وتتجنّب مزالق النطاق.

9 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

الإغلاق (closure) ليس حيلة غريبة: إنه الآلية التي تحتفظ بها الدالة بالوصول إلى النطاق المعجمي الذي عُرّفت فيه، لا الذي استُدعيت فيه. المحرك لا ينسخ القيم: بل يحتفظ بمرجع حي للبيئة المعجمية. لذلك ينتج عن استدعاءين لنفس المصنع بيئتان مستقلتان، وهذا ما يجعل الإغلاقات أخف أداة تغليف في اللغة.

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
حالة خاصة: لا يمكن الوصول إلى count إلا عبر التوابع المُعادة.

المزلق الكلاسيكي في الواقع العملي: يرتبط var بالدالة المحيطة لا بالكتلة. وداخل حلقة، يلتقط كل إغلاق حينئذٍ نفس المتغير. أما مع let، فتنشئ كل تكرارة بيئة معجمية جديدة، فيصبح كل التقاط منفصلا. أضف إلى ذلك المنطقة الميتة الزمنية (TDZ): يوجد متغير let أو const بمجرد الدخول إلى الكتلة، لكنه يرمي ReferenceError إن قرأته قبل تصريحه.

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
‏var يتشارك ارتباطا واحدا؛ let ينشئ ارتباطا لكل تكرارة.

من ناحية الأنماط، الإغلاقات أساس التذكير (memoization) ونمط الوحدة. مقارنة بالصنف، يمنحك الإغلاق خصوصية حقيقية بلا this ولا نماذج أولية، لكن بكلفة استهلاك ذاكرة أعلى قليلا حين تنشئ آلاف المثيلات: إذ يحمل كل مثيل دواله الخاصة.

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
يعيش الخزن المؤقت داخل الإغلاق، غير مرئي من الخارج.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. في حلقة for (var i = ...)، ماذا تطبع ثلاثة استدعاءات setTimeout أُنشئت داخل جسم الحلقة؟
    • 0، 1، 2 لأن كل استدعاء يلتقط قيمته الخاصة
    • 3، 3، 3 لأنها جميعا تتشارك نفس ارتباط i
    • undefined ثلاث مرات لأن i يُدمَّر بعد الحلقة
  2. ما الذي يحتفظ به الإغلاق بالضبط من البيئة التي عُرّف فيها؟
    • نسخة مجمّدة للقيم وقت الإنشاء
    • مرجع حي للمتغيرات الملتقَطة
    • معاملات الدالة المحيطة فقط
    • لا شيء: يُدمَّر النطاق حين تعود الدالة
  3. لماذا يضمن createCounter بقاء count خاصا؟
    • لأن let يجعل المتغير غير قابل للتغيير
    • لأنه لا يمكن الوصول إلى count إلا عبر الدوال المُعادة، وهي وحدها التي تلتقط نطاقه
    • لأن المحرك يحذف count بعد أن يعود المصنع