1379 words
7 minutes
let vs var vs const: The One Hack That Separates Juniors from Seniors

لو فاكر إن “المتغيرات” ده درس تمهيدي بناخده في أول يوم برمجة ونقلب الصفحة، اسمحلي أقولك إنك فايتك كتير.

في JavaScript تحديدًا، المتغيرات هي الخيط اللي لو مسكته صح، هتفهم اللغة دي شغالة إزاي من جوه: من أول الـ Memory Management لحد الـ Closures والـ Performance Optimization.

المقالة دي مش معمولة عشان تقولك “استخدم const ومستخدمش var”. المقالة دي معمولة عشان تشرحلك ليه، وإيه اللي بيحصل في الـ CPU والـ RAM لما بتكتب سطر كود بسيط زي let x = 10.

هناخدك في رحلة من “مجرد مستخدم للغة” لـ “مهندس فاهم الـ Engine”.


1. انسى “الصندوق”: فلسفة المتغيرات (Variable Binding)#

أشهر تشبيه بيتشرح للمبتدئين:

“المتغير عامل زي الصندوق، بنحط جواه قيمة.”

التشبيه ده “لطيف” للبدايات، بس كارثي لو عايز تبقى Senior. لأنه بيخليك تتخيل إن القيمة “جوه” المتغير.

في الحقيقة، المتغير في JavaScript (وفي معظم اللغات الحديثة) هو مجرد Binding أو “رباط”. تخيله كـ Label أو “لافتة” مربوطة بخيط، والخيط ده واصل لمكان معين في الذاكرة (Memory Address).

لما بتقول:

let user = { name: 'Ali' };

إنت بتعمل 3 حاجات:

  1. بتنشئ Object { name: 'Ali' } في مكان ما في الذاكرة (في الـ Heap).
  2. بتنشئ اسم user في الـ Current Scope.
  3. بتربط (Bind) الاسم user بمكان الـ Object ده.

ليه التفصيلة دي تفرق؟ لأنك لو فاكر إنه صندوق، مش هتفهم يعني إيه:

const a = { x: 1 };
const b = a; // إحنا مش بننسخ الصندوق، إحنا بنعمل خيط جديد لنفس المكان
b.x = 2;
console.log(a.x); // 2

لو هما صندوقين، المفروض a ميتتأثرش. بس لأنهم “خيطين” واصلين لنفس المكان، أي تعديل من أي خيط بيسمّع في المكان نفسه.


2. دورة حياة المتغير: The 3 Pillars#

أي متغير بيمر بـ 3 مراحل، والـ JavaScript Engine بيتعامل مع كل مرحلة بشكل منفصل، وده سر مشاكل كتير زي الـ TDZ والـ Hoisting:

  1. Declaration (الإعلان): اللغة بتسجل إن “في متغير اسمه x موجود في الـ Scope ده”. بس لسه مفيش قيمة، ورسميًا هو لسه مش جاهز للاستخدام.

  2. Initialization (التهيئة): اللغة بتحجز مكان في الذاكرة للمتغير ده، وبتحطله قيمة مبدئية (غالبًا undefined في الـ var، أو بتسيبه “uninitialized” في let/const).

  3. Assignment (الإسناد): اللغة بتحط القيمة الحقيقية اللي إنت كتبتها (مثلاً 10 أو "Hello") في المكان ده.

المشاكل بتحصل لما بنحاول نستخدم متغير عدى بمرحلة الـ 1 بس لسه معداش بـ 2، أو عدى بـ 1 و 2 بس لسه 3.


3. أين تعيش المتغيرات؟ (Stack vs Heap)#

عشان تبقى جامد، لازم تعرف الكود بتاعك بياكل رامات إزاي. الـ JavaScript Engine (زي V8 في Chrome) بيقسم الذاكرة لنوعين:

أ) Stack Memory (السهل الممتنع)#

دي ذاكرة سريعة جدًا، منظمة، وبسيطة. بيتخزن فيها:

  • الـ Primitives (الأرقام، الـ Booleans، الـ undefined، الـ null، والـ Symbols) لأن حجمهم ثابت ومعروف. (ملحوظة للمحترفين: الـ Primitives ممكن تروح الـ Heap لو جوه Closure، والـ V8 هو اللي بيقرر ده).
  • الـ References (مؤشرات) للأشياء الكبيرة.

ب) Heap Memory (المخزن الكبير)#

دي ذاكرة عشوائية وكبيرة، بيتخزن فيها أي حاجة حجمها مش ثابت أو كبير:

  • Objects
  • Arrays
  • Functions

مثال للتوضيح:

let age = 25;           // بتتخزن كلها في الـ Stack
let user = { id: 1 };  // المتغير 'user' (كمؤشر) في الـ Stack، والقيمة `{ id: 1 }` في الـ Heap

ليه ده مهم؟ عشان الـ Mutability. لما بتعدل في Primitive، إنت فعليًا بتغير القيمة في الـ Stack (أو بتشاور على مكان جديد). لكن لما بتعدل في Object، إنت بتسيب الـ Pointer اللي في الـ Stack زي ما هو، وبتروح تعدل في الداتا اللي في الـ Heap. عشان كده const بتسمح بتعديل الـ Objects (لأن الـ Pointer اللي في الـ Stack متغيرش)، بس مبتسمحش بـ Reassignment (لأن ده معناه تغيير الـ Pointer).


4. Scope & Scope Chain: الفيزياء الخاصة بالكود#

الـ Scope هو “مدى الرؤية”. مين شايف مين؟ قاعدة ذهبية: “اللي جوه شايف اللي بره، بس اللي بره مش شايف اللي جوه”.

لما الـ Engine بييجي يدور على قيمة متغير، بيمشي في رحلة اسمها Different scopes lookup أو Scope Chain:

  1. يدور في الـ Local Scope الحالي.
  2. ملقاش؟ يطلع للـ Parent Scope.
  3. ملقاش؟ يفضل يطلع لحد ما يوصل للـ Global Scope.
  4. ملقاش؟ يرمي ReferenceError.

ده بيفسر ليه الـ Global Variables “غالية” في الـ Performance (لأن الـ Engine بيمشي المشوار كله عشان يوصلها)، وليه هي “خطر” (لأن أي حد في السلسلة ممكن يغيرها).


5. رحلة عبر الزمن: Hoisting والـ Dead Zone#

خلينا نقفل ملف var vs let vs const للأبد، بس من منظور الـ Architecture مش الحفظ.

أولاً: var (الإرث الثقيل)#

  • Scope: Function Scope. يعني if و for مش بيعملوا scope جديد ليها. بتدلق بره البلوكات دي.

  • Hoisting: بيحصلها “رفع” لأول الـ Function.

    • تريك: هي بتترفع كـ Declaration و Initialization بـ undefined.
    • عشان كده لو استخدمتها قبل ما تعرفها، الكود مش بيضرب Error، بس بيرجع undefined. وده أخطر نوع Bugs.

    كارثة تانية لـ var:

    var x = 10;
    console.log(window.x); // 10 (Browser Global Pollution)
    

    الـ var لما بتبقى global بتلزق نفسها في الـ window object، وده ممكن يبوظ مكتبات تانية شغالة في الصفحة. let و const أنضف ومش بيعملوا كده.

ثانياً: let & const (العصر الحديث)#

  • Scope: Block Scope. بتحترم الـ {}.
  • Hoisting: بيحصلها Hoisting؟ آه والله! بس الفرق في الـ Initialization.
    • الـ Engine عارف إن المتغير موجود (Declaration تم)، بس المتغير لسه في حالة “Uninitialized”.
    • هو محجوز له مكان، بس الـ Engine حاطط عليه “شمع أحمر” ممنوع الاقتراب منه لحد سطر التعريف.
    • الفترة الزمنية دي (من بداية الـ Scope لحد سطر التعريف) اسمها TDZ (Temporal Dead Zone). منطقة الموت المؤقت. أي محاولة لمس المتغير فيها بتعمل ReferenceError.

ثالثاً: const لا تعني “ثابت”#

const بتحمي الـ Binding فقط.

  • لو const x = 10، مستحيل تغير x.
  • لو const arr = []، عادي جدًا تعمل arr.push(1). الصندوق نفسه مفتوح، بس ممنوع تبدل الصندوق بصندوق تاني.

للمحترفين فقط: لو عايز تحمي الـ Object نفسه من التعديل (Immutability)، استخدم Object.freeze(arr)، دي حاجة وتلك حاجة.

رابعاً: var vs let في الـ Loops#

جرب الكود ده:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// النتيجة: 3, 3, 3 😱

لأن var معندهاش block scope، فالـ i هي نفس المتغير لكل اللفات. لما الـ timeout اشتغل، كانت الـ loop خلصت و i بقت 3.

عدلها لـ let:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// النتيجة: 0, 1, 2 ✅

هنا let بتعمل scope “جديد” لكل لفة. كل i هي نسخة منفصلة ومعزولة عن التانية.


6. Closures: السحر الحقيقي#

الـ Closure هو الدليل القاطع إن دوال JavaScript مش مجرد سطور كود، دي كائنات حية شايلة “شنطة ذكريات”.

لما Function بتتعرف جوه Function تانية، وتخرج منها (Return)، بتفضل محتفظة بـ Reference للـ Scope اللي اتولدت فيه.

function outer() {
  let count = 0; // المفروض المتغير ده يموت بانتهاء الدالة
  return function inner() {
    count++;
    console.log(count);
  };
}

const counter = outer();
counter(); // 1
counter(); // 2

إزاي count لسه عايش؟ لأن inner عملت عليه “Closure”. الـ Engine كان ذكي، لاحظ إن inner محتاجة count، فاحتفظ بـ Reference للـ Lexical Scope اللي فيه count.

⚠️ تحذير هام جدًا: الـ Closures سلاح ذو حدين. لو الـ Closure محتفظ بـ reference لحاجات كبيرة مش محتاجها، ده بيعمل Memory Leak لأن الـ Garbage Collector مش هيقدر يمسحها طول ما الـ Closure عايش. استخدمها بحكمة.

تطبيق عملي (Data Privacy): الـ Closure هو الطريقة الوحيدة (قبل الـ Private Fields #) عشان تعمل متغيرات خاصة محدش يقدر يلعب فيها من بره.


7. Zero-Cost Abstractions: نصائح للـ Seniors#

إزاي تكتب كود متغيرات Efficient؟

  1. Avoid Global Variables: مش بس عشان النظافة، عشان الـ Lookup time في الـ Scope Chain، وعشان بتمنع الـ Engine من إنه يعمل Optimizations معينة و Garbage Collection فعال.
  2. Declare Variables Close to Usage: متعملش زي لغة C زمان وتعرف كل حاجة فوق. عرف المتغير قبل استخدامه مباشرة. ده بيقلل الـ Live Range بتاعه وبيسهل على الـ Garbage Collector شغله.
  3. Use const by Default: ده مش بس Style. ده بيسهل على الـ Engine يعرف إن الـ Binding ده مش هيتغير، وساعات بيساعد في الـ Optimization.
  4. Hidden Classes (V8 Magic): لو بتعمل Objects، حاول تثبت شكلها (الـ Keys وترتيبها).
    // ❌ وحش - كل Object ليه شكل مختلف (Hidden Class مختلف)
    function Point(x, y) {
      this.x = x;
      if (y) this.y = y; // مرة بـ y ومرة من غير
    }
    
    // ✅ صح - شكل ثابت دايماً
    function Point(x, y) {
      this.x = x;
      this.y = y ?? 0; // دايماً في x و y بنفس الترتيب
    }
    
    الـ V8 بيعمل “Hidden Classes” عشان يسرع الوصول للـ Properties. تغيير شكل الـ Object بيجبر الـ Engine يعمل كلاسات جديدة، وده بيبطئ الكود في الـ High Performance Apps.

الخلاصة#

المتغير مش مجرد “مخزن قيم”. هو عقد (Contract) بينك وبين الـ Engine:

  • بـ var، إنت بتعمل عقد متساهل، قديم، ومليان ثغرات.
  • بـ let، إنت بتعمل عقد محدد بمدة ومكان (Block Scoped) وقابل للتجديد (Reassignable).
  • بـ const، إنت بتعمل عقد صارم ومحدد وغير قابل للفسخ (Immutable Binding).

افهم الـ Memory Model، وافهم الـ Scope Chain، وهتلاقي مشاكل الـ undefined والـ Race Conditions اختفت من حياتك.

نصيحة أخيرة: الكود بيتكتب مرة وبيتقري 100 مرة. استخدم أسماء متغيرات واضحة توصف المحتوى بدقة. let d وحشة، let daysSinceLogin تحفة فنية.

Join our whatsapp group here
My Channel here