المنطقة الميتة الزمنية (TDZ) في JavaScript: فهم عميق وآليات العمل

دقائق القراءة: 8

ما هي المنطقة الميتة الزمنية (TDZ) في JavaScript؟

قد يبدو مصطلح المنطقة الميتة الزمنية (Temporal Dead Zone – TDZ) وكأنه مستوحى من أفلام الخيال العلمي، لكنه مفهوم أساسي وحيوي لفهم كيفية عمل JavaScript. في عالم البرمجة، يُعد فهم المصطلحات والمفاهيم التي نتعامل معها يوميًا، أو نسعى لتعلمها، أمرًا بالغ الأهمية. استعد جيدًا، لأننا سنتعمق في تفاصيل هذا المفهوم الذي قد يبدو معقدًا للوهلة الأولى.

هل تعلم أنه في JavaScript، يمكننا استخدام الأقواس المعقوفة { } لإنشاء مستوى جديد من النطاق (scope) في أي مكان نريده؟ على سبيل المثال، يمكننا دائمًا فعل الآتي:

{ { { { { { var madness = true } } } } } }

لقد أدرجت هذه التفصيلة للتأكد من أن الأمثلة القادمة ستكون مفهومة للجميع، حيث لم أرد أن أفترض أن الجميع على دراية بهذا الأمر. قبل إصدار ES6، لم تكن هناك طريقة أخرى للإعلان عن المتغيرات سوى باستخدام الكلمة المفتاحية var. لكن ES6 أحدث ثورة بتقديم let و const.

فهم نطاق المتغيرات: var مقابل let و const

تتميز إعلانات let و const بكونها ذات نطاق كتلي (block-scoped)، مما يعني أنها لا يمكن الوصول إليها إلا ضمن الكتلة { } التي تحيط بها. على النقيض من ذلك، لا تملك var هذا القيد، مما يجعل سلوكها مختلفًا تمامًا. لنلقِ نظرة على الأمثلة التالية لتوضيح الفارق:

النطاق الكتلي (Block Scope) لـ let و const

let babyAge = 1;
let isBirthday = true;

if (isBirthday) {
    let babyAge = 2;
}

console.log(babyAge); // Hmmmm. This prints 1

في المثال أعلاه، نلاحظ أن قيمة babyAge المطبوعة هي 1. هذا يحدث لأن إعادة التصريح عن babyAge بالقيمة 2 تكون متاحة فقط داخل كتلة if. خارج هذه الكتلة، يتم استخدام المتغير babyAge الأول الذي تم التصريح عنه بقيمة 1. هل يمكنك أن ترى بوضوح أنهما متغيران مختلفان تمامًا، على الرغم من حملهما لنفس الاسم؟

غياب النطاق الكتلي لـ var

على النقيض من ذلك، لا تملك إعلانات var نطاقًا كتليًا، مما يؤدي إلى سلوك مختلف:

var babyAge = 1;
var isBirthday = true;

if (isBirthday) {
    var babyAge = 2;
}

console.log(babyAge); // Ah! This prints 2

هنا، يتم طباعة القيمة 2. هذا لأن var لا تتقيد بالنطاق الكتلي، وبالتالي فإن إعادة التصريح عن babyAge داخل كتلة if يغير قيمة المتغير الأصلي في النطاق الخارجي.

الفرق الجوهري: الوصول قبل التصريح

الاختلاف الجوهري الأخير بين let/const و var يكمن في سلوك الوصول إلى المتغير قبل التصريح به. إذا حاولت الوصول إلى متغير var قبل الإعلان عنه، فستكون قيمته undefined. أما إذا فعلت الشيء نفسه مع let و const، فسيتم إلقاء خطأ ReferenceError.

console.log(varNumber); // undefined
console.log(letNumber); // Doesn't log, as it throws a ReferenceError: Cannot access 'letNumber' before initialization

var varNumber = 1;
let letNumber = 1;

هذا الخطأ الذي يتم إلقاؤه مع let و const هو السبب الرئيسي لوجود المنطقة الميتة الزمنية (Temporal Dead Zone).

شرح المنطقة الميتة الزمنية (TDZ)

إذن، ما هي TDZ؟ إنها المصطلح الذي يصف الحالة التي تكون فيها المتغيرات غير قابلة للوصول. تكون هذه المتغيرات موجودة في النطاق (in scope)، لكنها لم يتم التصريح عنها بعد. متغيرات let و const توجد في TDZ من بداية نطاقها المحيط حتى يتم التصريح عنها فعليًا.

يمكننا القول أيضًا إن المتغيرات توجد في TDZ من النقطة التي يتم فيها ربطها بالنطاق الذي توجد فيه (عندما يتم ربط المتغير بالنطاق الذي هو جزء منه) حتى يتم التصريح عنها (عندما يتم حجز اسم للمتغير في الذاكرة).

{
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    let age = 25; // لقد وصلنا! لا مزيد من TDZ
    console.log(age);
}

كما ترى في المثال أعلاه، إذا حاولت الوصول إلى المتغير age قبل التصريح به، فسيتم إلقاء خطأ ReferenceError. هذا يحدث بسبب TDZ. لكن var لا تفعل ذلك؛ فمتغيرات var يتم تهيئتها افتراضيًا إلى undefined، على عكس طرق التصريح الأخرى.

الفرق بين التصريح (Declaring) والتهيئة (Initializing)

من المهم التمييز بين التصريح عن المتغير وتهيئته. إليك مثال يوضح ذلك:

function scopeExample() {
    let age; // 1: تصريح (Declaration)
    age = 20; // 2: تهيئة (Initialization)
    let hands = 2; // 3: تصريح وتهيئة في سطر واحد
}

التصريح عن متغير يعني أننا نحجز الاسم في الذاكرة ضمن النطاق الحالي. هذا ما يمثله السطر رقم 1 في التعليقات. أما تهيئة متغير، فتعني تعيين قيمة لهذا المتغير. هذا ما يمثله السطر رقم 2.

يمكنك دائمًا القيام بكليهما في سطر واحد، كما هو موضح في السطر رقم 3. للتذكير مرة أخرى: متغيرات let و const توجد في TDZ من بداية نطاقها المحيط حتى يتم التصريح عنها.

تطبيق مفهوم TDZ على المثال السابق

بناءً على ما سبق، أين تقع TDZ للمتغير age في مقتطف الكود أعلاه؟ وهل يمتلك المتغير hands منطقة ميتة زمنية؟ إذا كان الأمر كذلك، فأين تبدأ وأين تنتهي؟

الإجابة: كلا المتغيرين hands و age يدخلان TDZ. تنتهي TDZ للمتغير hands عندما يتم التصريح عنه وتهيئته في نفس السطر (السطر 3). أما TDZ للمتغير age فتنتهي عندما يتم التصريح عنه وحجز اسمه في الذاكرة (الخطوة 1).

لماذا تنشأ المنطقة الميتة الزمنية (TDZ)؟ دور الـ Hoisting

دعنا نعود إلى مثالنا الأول:

{
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    // هذه هي المنطقة الميتة الزمنية للمتغير age!
    let age = 25; // لقد وصلنا! لا مزيد من TDZ
    console.log(age);
}

إذا أضفنا console.log داخل TDZ، فسترى هذا الخطأ:

لقطة شاشة لخطأ ReferenceError: Cannot access 'age' before initialization في JavaScript

لماذا توجد TDZ بين أعلى النطاق والتصريح عن المتغير؟ ما هو السبب المحدد لذلك؟ السبب هو عملية الرفع (Hoisting).

محرك JavaScript الذي يقوم بتحليل وتنفيذ الكود الخاص بك يمر بخطوتين رئيسيتين:

  1. تحليل الكود: يتم تحويل الكود إلى شجرة بناء مجردة (Abstract Syntax Tree) أو بايت كود قابل للتنفيذ.
  2. تنفيذ وقت التشغيل: يتم تنفيذ الكود.

الخطوة الأولى هي حيث تحدث عملية الرفع (Hoisting)، ويتم ذلك بواسطة محرك JS. تقوم هذه العملية أساسًا بتحريك جميع إعلانات المتغيرات إلى أعلى نطاقها. على سبيل المثال:

console.log(hoistedVariable); // undefined
var hoistedVariable = 1;

لتوضيح الأمر، هذه المتغيرات لا تتحرك فعليًا في الكود المكتوب. لكن النتيجة ستكون مطابقة وظيفيًا لما يلي:

var hoistedVariable;
console.log(hoistedVariable); // undefined
hoistedVariable = 1;

الفرق بين let و const و var في عملية الـ Hoisting

الفرق الوحيد بين const و let و var هو أنه عندما يتم رفعها (hoisted)، لا يتم تعيين قيمها افتراضيًا إلى undefined. لإثبات أن let و const يتم رفعهما أيضًا، إليك مثال:

{
    // سيتم رفع كلا المتغيرين أدناه إلى أعلى نطاقهما!
    console.log(typeof nonsenseThatDoesntExist); // Prints undefined
    console.log(typeof name); // Throws an error, cannot access 'name' before initialization
    let name = "Kealan";
}

المقتطف أعلاه هو دليل على أن let يتم رفعه بوضوح فوق المكان الذي تم التصريح عنه فيه، حيث ينبهنا المحرك إلى هذه الحقيقة. إنه يعلم أن name موجود (تم التصريح عنه)، لكن لا يمكننا الوصول إليه قبل تهيئته. إذا كان ذلك يساعدك على التذكر، فكر في الأمر على النحو التالي:

  • عندما يتم رفع المتغيرات، يتم تهيئة var بقيمة undefined افتراضيًا في عملية الرفع.
  • يتم رفع let و const أيضًا، لكن لا يتم تعيينهما إلى undefined عند رفعهما.

وهذا هو السبب الوحيد لوجود TDZ، ولهذا السبب تحدث مع let و const وليس مع var.

أمثلة إضافية على المنطقة الميتة الزمنية (TDZ)

يمكن أن تنشأ TDZ أيضًا لمعاملات الدوال الافتراضية. على سبيل المثال:

مع معاملات الدوال الافتراضية

function createTDZ(a = b, b) {
}
createTDZ(undefined, 1);

هذا الكود سيُلقي خطأ ReferenceError، لأن تقييم المتغير a يحاول الوصول إلى المتغير b قبل أن يتم تحليله بواسطة محرك JS. جميع وسائط الدالة تكون داخل TDZ حتى يتم تحليلها. حتى شيء بسيط مثل let tdzTest = tdzTest; سيُلقي خطأ بسبب TDZ، بينما var tdzTest = tdzTest; سيُنشئ tdzTest ويُعيّن قيمته إلى undefined.

مثال متقدم من Erik Arvindson

إليك مثال أخير ومتقدم إلى حد ما من Erik Arvindson (المشارك في تطوير وصيانة مواصفات ECMAScript):

let a = f(); // 1
const b = 2;
function f() {
    return b;
} // 2, b is in the TDZ

يمكنك تتبع الأرقام المعلقة. في السطر الأول، نستدعي الدالة f()، ثم نحاول الوصول إلى المتغير b (مما يؤدي إلى إلقاء خطأ ReferenceError لأن b يقع في TDZ في تلك اللحظة).

لماذا نحتاج إلى المنطقة الميتة الزمنية (TDZ)؟

قدم الدكتور Alex Rauschmayer منشورًا ممتازًا يشرح سبب وجود TDZ، والسبب الرئيسي هو:

  • المساعدة في اكتشاف الأخطاء: محاولة الوصول إلى متغير قبل التصريح به هي طريقة خاطئة ويجب ألا تكون ممكنة. TDZ تفرض هذه القاعدة، مما يجعل الكود أكثر استقرارًا وأقل عرضة للأخطاء غير المتوقعة.
  • منح دلالات أكثر منطقية لـ const: نظرًا لأن const يتم رفعها، فماذا يحدث إذا حاول المبرمج استخدامها قبل التصريح بها في وقت التشغيل؟ ما هي القيمة التي يجب أن تحملها في النقطة التي يتم رفعها فيها؟ TDZ توفر نهجًا واضحًا ومنطقيًا لهذا السيناريو، وقد كان هذا هو أفضل نهج قرره فريق مواصفات ECMAScript.

كيف تتجنب المشاكل التي تسببها TDZ؟

بشكل بسيط نسبيًا، تأكد دائمًا من تعريف متغيرات let و const في أعلى نطاقها. هذه الممارسة الجيدة لا تساعد فقط في تجنب مشاكل TDZ، بل تجعل الكود الخاص بك أكثر قابلية للقراءة والصيانة.

الخلاصة التقنية

تُعد المنطقة الميتة الزمنية (TDZ) مفهومًا محوريًا في JavaScript الحديثة، وهي تمثل حجر الزاوية في فهم سلوك المتغيرات المُصرح عنها باستخدام let و const. على الرغم من أن اسمها قد يوحي بالتعقيد، إلا أن فهمها يُمكّن المطورين من كتابة أكواد أكثر قوة وموثوقية، ويُقلل بشكل كبير من الأخطاء المنطقية التي قد تنشأ عن الوصول المبكر للمتغيرات. إنها آلية تصميمية ذكية تفرض أفضل الممارسات البرمجية وتُعزز من استقرار التطبيقات، مما يجعلها إضافة لا غنى عنها للغة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *