جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة
من الشجرة النحوية المجردة (AST) إلى كود الآلة: استكشف كيف ينفذ محرك V8 التعليمات، ولماذا تحدث الـ Deoptimization، وكيف يتم توزيع الذاكرة بين المكدس والكومة.
Elgun Mammadli
Front End Developer

يبدأ معظم المطورين رحلتهم مع جافاسكريبت بكتابة console.log("Hello World")، وتعريف المتغيرات، واستدعاء الدوال البسيطة. لكن الوصول إلى مستوى مهندس أول (Senior Engineer) يتطلب تجاوز المظهر الخارجي للكود وفهم ما يحدث خلف الستار: ماذا يعني الكود الذي نكتبه للعتاد الفعلي (Hardware)، وكيف ينفذه محرك جافاسكريبت خطوة بخطوة؟
في هذا المقال، نقوم بتشريح المعمارية الداخلية للغة: خط تجميع وتحسين محرك V8، ودورة حياة سياق التنفيذ (Execution Context)، والقواعد الأساسية لإدارة الذاكرة.
1. داخل المحرك: من الكود المصدري إلى لغة الآلة
لم تعد جافاسكريبت لغة تُفسر سطراً بسطر كما كانت في السابق، بل تعتمد المحركات الحديثة على أسلوب JIT (Just-In-Time) Compilation. ويعد محرك V8 المطور من Google أشهر هذه المحركات (وهو المحرك الذي يدير Chrome وNode.js وDeno).
عند إدخال الكود إلى بيئة التشغيل، يمر بسلسلة العمليات التالية:
المحلل اللغوي (Parser) وشجرة البناء المجردة (AST)
يبدأ المحرك بالتحليل المعجمي (Lexical Analysis)، حيث يفكك النص البرمجي إلى رموز أولية تُعرف بالـ Tokens. بعد ذلك، يقوم الـ Parser بتحويل هذه الرموز إلى هيكل بياني هرمي يُدعى AST (Abstract Syntax Tree) يمثل القواعد النحوية للكود بدقة.
المفسر Ignition
يستقبل المفسر Ignition شجرة الـ AST ويحولها بسرعة إلى Bytecode مستقل عن المنصة. هذا الكود الوسيط يُنفذ فوراً دون الحاجة للانتظار حتى انتهاء التجميع الكامل، مما يمنح التطبيق إقلاعاً فائق السرعة.
المجمّع TurboFan ونظام Profiler
أثناء تشغيل الـ Bytecode، يقوم نظام داخلي يُسمى Profiler بمراقبة الأجزاء التي تتكرر بكثرة — وتُعرف باسم "Hot Code" — ويسجل معلومات الأنواع البرمجية (Type Feedback).
إذا كانت الدالة تُستدعى دائماً بنفس أنواع البيانات، يتدخل المجمع المحسّن TurboFan ويحول ذلك الـ Bytecode مباشرة إلى Machine Code (كود آلة فائق السرعة) مصمم خصيصاً للمعالج.
فخ إلغاء التحسين (Deoptimization)
لغة جافاسكريبت ديناميكية النوع (Dynamically Typed). إذا قام TurboFan بتحسين دالة للتعامل مع الأرقام (number)، ثم تم تمرير نصوص لها فجأة (string):
يقوم المحرك بالتراجع الفوري (Bailout) وإلغاء الكود المجمع، ليعود مجدداً لتفسير الـ Bytecode الأبطأ. لهذا السبب، فإن كتابة كود أحادي الشكل (Monomorphic) وثابت البنية يُعد أحد أهم أسرار الأداء العالي.
2. ما هو سياق التنفيذ (Execution Context)؟
منذ اللحظة التي يبدأ فيها تشغيل الكود، تُدار جميع العمليات داخل بيئات معزولة تُسمى Execution Context (EC). بعبارة مبسطة، سياق التنفيذ هو "الحاوية" التي يتم فيها فحص وتشغيل الكود الحالي.
يوجد نوعان رئيسيان:
- سياق التنفيذ العام (GEC - Global Execution Context): ينشأ تلقائياً عند بدء تشغيل الملف. يوجد سياق عام واحد فقط يحدد الكائن العام (
windowفي المتصفح أوglobalفي Node.js). - سياق تنفيذ الدالة (FEC - Function Execution Context): يتم إنشاؤه في كل مرة يُستدعى فيها دالة معينة.
يمر كل سياق تنفيذ بـ مرحلتين أساسيتين:
المرحلة الأولى: مرحلة الإنشاء (Creation Phase)
قبل تنفيذ أي سطر برمجي، يمسح المحرك النطاق ويقوم بحجز الذاكرة:
- ربط الكائن العام (
windowأوglobal). - تحديد مرجع
thisالخاص بالنطاق. - بناء بيئة المتغيرات والبيئة المعجمية (Lexical Environment و Variable Environment).
- تخصيص مساحات الذاكرة للمتغيرات والدوال (وهنا تحديداً يحدث مفهوم الـ Hoisting).
المرحلة الثانية: مرحلة التنفيذ (Execution Phase)
يقوم المحرك بقراءة الكود سطراً بسطر من الأعلى إلى الأسفل، وإسناد القيم الفعلية للمتغيرات واستدعاء الدوال.
3. الرفع (Hoisting) والمنطقة الميتة الزمنية (TDZ)
الرفع (Hoisting) ليس نقلاً مادياً للكود إلى أعلى الملف، بل هو نتيجة مباشرة لحجز الذاكرة أثناء Creation Phase قبل تشغيل الكود.
يكمن الاختلاف الجوهري في كيفية تهيئة التعريفات خلال هذه المرحلة:
- الكلمة المفتاحية
var: يتم تسجيلها في الذاكرة وتهيئتها فوراً بالقيمةundefined. لذلك، عند قراءتها قبل سطر التعريف تعيدundefinedدون حدوث خطأ برمجي. - الكلمتان
letوconst: يتم حجز مساحة لهما في الذاكرة أيضاً، لكنهما تظلان بدون تهيئة. الفترة الفاصلة بين دخول النطاق وسطر التعريف الفعلي تُسمى Temporal Dead Zone (TDZ)، ومحاولة الوصول للمتغير داخلها يطلق الخطأReferenceError.
4. مكدس الاستدعاءات وبنية الذاكرة: Stack مقابل Heap
جافاسكريبت لغة أحادية المسار (single-threaded) — أي أنها تنفذ عملية واحدة فقط في نفس اللحظة. مسار التنفيذ يُدار عبر Call Stack (مكدس الاستدعاءات)، بينما تُخزن البيانات المركبة داخل Memory Heap (الكومة).
| الخاصية | Call Stack (المكدس) | Memory Heap (الكومة) |
|---|---|---|
| الوظيفة | تدفق التنفيذ، الأنواع الأولية ومؤشرات العناوين (Pointers) | الكائنات المعقدة، المصفوفات، والدوال |
| الهيكل | هيكل صارم بنظام LIFO (ما يدخل آخراً يخرج أولاً) | مساحة غير منظمة وقابلة للنمو الديناميكي |
| السرعة | وصول مباشر وفائق السرعة | أبطأ نسبياً بسبب تتبع عناوين المؤشرات |
يحتفظ الـ Stack بعنوان الذاكرة فقط (0x0012FA). وعند مساواة كائن بمتغير آخر، يتم نسخ المؤشر فقط وليس بيانات الكائن الفعلية:
طفحان المكدس (Stack Overflow)
كل استدعاء لدالة يضيف إطاراً جديداً (Stack Frame) داخل الـ Call Stack، ويتم إخراجه فور انتهاء الدالة. وإذا حدث استدعاء تكراري لانهائي (Infinite Recursion)، تنفد ذاكرة المكدس ويحدث الخطأ الشهير Maximum call stack size exceeded.
5. إدارة الذاكرة: خوارزمية Mark-and-Sweep
لا يمكن للبيانات أن تتراكم في الـ Heap إلى الأبد. يقوم مجمع المهملات (Garbage Collector - GC) دورياً بالتعرف على مساحات الذاكرة غير المستخدمة وتحريرها.
تعتمد المحركات الحديثة على خوارزمية Mark-and-Sweep (التعليم والكنس):
- الجذور (Roots): المتغيرات العامة (Global) والمتغيرات المحلية الموجودة في إطارات المكدس النشطة تمثل نقطة البداية.
- التعليم (Mark): يتتبع مجمع المهملات الروابط انطلاقاً من الجذور، ويضع علامة "قابل للوصول" (Reachable) على كل كائن يصادفه، ثم يفحص تفرعاته بشكل متكرر.
- الكنس (Sweep): الكائنات غير القابلة للوصول تُعتبر غير مستخدمة، وتُمسح مساحتها من الذاكرة بالكامل.
خطر تسريب الذاكرة (Memory Leaks)
إذا لم تعد هناك حاجة لبيانات معينة، ولكن بقي لها مرجع مرتبط بمستمع أحداث منسي (addEventListener)، أو متغير عام غير مقصود، أو داخل دالة إغلاق (Closure) غير مغلقة، فلن يتمكن الـ GC من حذفها، مما يؤدي إلى تراكم الذاكرة وتراجع أداء المتصفح.
مشاركة المقال
جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة