الوحدة 2 · المنطق والدوال
النطاق (Scope) والـ Closures
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تتوقّع أين يمكن استخدام متغيّر ما (نطاق الكتلة ونطاق الدالة والنطاق العام)، وتتجنّب مزالق var القديمة، وتستخدم الـ closures لتمنح الدوال حالة خاصة تتذكّرها، مثل عدّاد للمعرّفات.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل مصروف في Pocket يحتاج إلى معرّف (id) فريد كي تستطيع لاحقًا تعديله هو بالذات أو حذفه. والطريقة البديهية عدّاد في أعلى الملف:
let nextId = 1;
function createId() {
return `exp-${nextId++}`;
}هذا يعمل، إلى أن يأتي جزء آخر من البرنامج، ربما ميزة “إعادة التعيين” كُتبت بعد أشهر، فيضبط nextId = 1 من جديد، فتحصل على مصروفين باسم exp-1. العدّاد مرئي في كل مكان، لذلك يستطيع أي شيء أن يُفسده. ولكي تحميه، تحتاج إلى فهم النطاق (scope): قواعد المكان الذي يمكن أن يُرى فيه اسم ما.
الكتل والدوال والنطاق العام
كل زوج من الأقواس المعقوفة يُنشئ كتلة (block)، وأسماء let وconst تعيش في الكتلة التي صُرّح بها فيها. والدوال تُنشئ نطاقًا أيضًا. أما الأسماء المُصرَّح بها خارج كل الكتل والدوال فهي في النطاق العام (global scope)، ومرئية في كل مكان.
const appName = "Pocket"; // global
function describe(amount) {
const size = amount > 10000 ? "big" : "small"; // function scope
if (size === "big") {
const warning = "Think twice"; // block scope
console.log(appName, size, warning);
}
// console.log(warning) here would throw a ReferenceError
}
describe(12000);حين يصادف المحرّك اسمًا، يبحث عنه في النطاق الحالي أولًا. فإن لم يجده هناك، بحث في النطاق المحيط، ثم في الذي يحيط به، وصولًا إلى النطاق العام. وإن لم يجده بعد ذلك، تحصل على ReferenceError: … is not defined. والبحث يتّجه إلى الخارج فقط: الكود داخل الدالة يستطيع قراءة appName العام، لكن الكود خارج الدالة لا يستطيع أبدًا رؤية size.
ولهذا جُعل سطر warning تعليقًا. فالكتلة التي صرّحت بـ warning انتهت، فاختفى الاسم.
إذا صرّح نطاق داخلي باسم موجود أصلًا في الخارج، فإن الاسم الداخلي يحجب (shadows) الخارجي: داخل الكتلة، يعني الاسم المتغيّر الداخلي. هذا مسموح، لكن وجود شيئين مختلفين باسم واحد يجعل الكود صعب القراءة، فاختر أسماء مختلفة.
مشكلة var
قبل عام 2015، لم يكن في JavaScript إلا var، وvar تتجاهل الكتل: إنها مرئية في الدالة التي هي فيها بالكامل. وهذا يُنتج خطأً شهيرًا. هنا تجدول الحلقة ثلاث دوال لتُنفَّذ بعد لحظة باستخدام setTimeout:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}حلقة var تطبع 3 ثلاث مرات، لأن هناك i واحدًا فقط للحلقة كلها، وحين تُنفَّذ الدوال يكون قد بلغ 3. أما حلقة let فتطبع 0 و1 و2، لأن let تُنشئ j جديدًا في كل دورة، وكل دالة تحتفظ بنسختها الخاصة. وتذكّر هذه الدوال للمتغيّرات هو بالضبط ما يُسمّى closure.
الـ Closures: دوال تتذكّر
الدالة التي تُنشأ داخل دالة أخرى تستطيع استخدام متغيّرات الدالة الخارجية. والمفاجأة أنها تحتفظ بهذا الوصول بعد أن تنتهي الدالة الخارجية:
function createIdGenerator(prefix) {
let count = 0;
return () => {
count++;
return `${prefix}-${count}`;
};
}
const nextExpenseId = createIdGenerator("exp");
const nextCategoryId = createIdGenerator("cat");
console.log(nextExpenseId());
console.log(nextExpenseId());
console.log(nextCategoryId());
console.log(nextExpenseId());تتبّع المحرّك. استدعاء createIdGenerator("exp") يُنشئ prefix وcount جديدين، ويبني الدالة السهمية، ويُرجعها. في العادة تختفي متغيّرات الدالة حين تنتهي، لكن الدالة السهمية ما زالت تشير إلى count وprefix، فيبقيان حيّين، ملتصقين بها. وهذا الاقتران بين دالة والمتغيّرات التي وُلدت بجوارها هو ما يُسمّى closure.
كل استدعاء لـ createIdGenerator يُنشئ مجموعة جديدة منفصلة من المتغيّرات. ولهذا يبدأ nextCategoryId() من 1، وتستمر معرّفات المصاريف من حيث توقّفت.
والآن انظر إلى الأمان الذي كسبته. لا يوجد count عام. لا يستطيع أي كود آخر قراءته، ولا تصفيره، ولا ضبطه على قيمة مكرّرة. والطريقة الوحيدة للتأثير فيه هي استدعاء الدالة التي أُعطيت لك، وهي لا تستطيع إلا أن تدفعه إلى الأمام. هذه هي الطريقة المعتمدة لمنح دالة حالة خاصة (private state) في JavaScript.
يستطيع الـ closure أن يحمي أكثر من مجرّد عدّاد. هذه ميزانية لا يمكن أن تنقص إلا عبر الدالة التي تسلّمها:
function createBudget(limitCents) {
let remaining = limitCents;
return (spentCents) => {
remaining -= spentCents;
return remaining;
};
}
const spend = createBudget(2000);
console.log(spend(450));
console.log(spend(1220));اكتمل القسم 2: تستطيع الآن اتخاذ القرارات، وتكرار العمل، وحزمه في دوال لها حالتها الخاصة. وينتقل القسم 3 إلى البيانات، بدءًا بالقائمة التي كنت تمرّ عليها: المصفوفة.
أهم النقاط
- النطاق (scope) هو المكان الذي يمكن فيه استخدام اسم ما:
letوconstتعيشان في أقرب كتلة{ }، والبحث عن الأسماء يتّجه إلى الخارج، لا إلى الداخل أبدًا. - الاسم الداخلي الذي يحمل اسم متغيّر خارجي “يحجبه” (shadowing) داخل النطاق الداخلي، وهذا مسموح لكن من السهل أن يُساء فهمه.
varتتجاهل الكتل ويمتد نطاقها إلى الدالة كلها، وهذا يسبّب أخطاء كلاسيكية؛ استخدمletوconst.- الـ closure دالة تحتفظ بإمكانية الوصول إلى متغيّرات النطاق الذي أُنشئت فيه، حتى بعد أن تكون الدالة الخارجية قد انتهت.
- كل استدعاء للدالة الخارجية يُنشئ مجموعة جديدة من المتغيّرات، لذلك يملك كل closure تُرجعه حالته الخاصة.