العودة إلى جميع المقالات
نظرة عامة على التكنولوجيا

جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة

من الشجرة النحوية المجردة (AST) إلى كود الآلة: استكشف كيف ينفذ محرك V8 التعليمات، ولماذا تحدث الـ Deoptimization، وكيف يتم توزيع الذاكرة بين المكدس والكومة.

E

Elgun Mammadli

Front End Developer

Oct 7, 2026
6 دقائق
جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة

يبدأ معظم المطورين رحلتهم مع جافاسكريبت بكتابة console.log("Hello World")، وتعريف المتغيرات، واستدعاء الدوال البسيطة. لكن الوصول إلى مستوى مهندس أول (Senior Engineer) يتطلب تجاوز المظهر الخارجي للكود وفهم ما يحدث خلف الستار: ماذا يعني الكود الذي نكتبه للعتاد الفعلي (Hardware)، وكيف ينفذه محرك جافاسكريبت خطوة بخطوة؟

في هذا المقال، نقوم بتشريح المعمارية الداخلية للغة: خط تجميع وتحسين محرك V8، ودورة حياة سياق التنفيذ (Execution Context)، والقواعد الأساسية لإدارة الذاكرة.


1. داخل المحرك: من الكود المصدري إلى لغة الآلة

لم تعد جافاسكريبت لغة تُفسر سطراً بسطر كما كانت في السابق، بل تعتمد المحركات الحديثة على أسلوب JIT (Just-In-Time) Compilation. ويعد محرك V8 المطور من Google أشهر هذه المحركات (وهو المحرك الذي يدير Chrome وNode.js وDeno).

عند إدخال الكود إلى بيئة التشغيل، يمر بسلسلة العمليات التالية:

pipeline-architecture.txt
Source Code 
    │
    ▼
[ Parser / Scanner ]  ──►  Token stream & AST (Abstract Syntax Tree)
    │
    ▼
[ Ignition ] (Interpreter) ──►  Bytecode  ──► Executes
    │
    ├─► Profiler (Type Feedback & "Hot Code" analysis)
    │
    ▼
[ TurboFan ] (Optimizing Compiler) ──► Highly Optimized Machine Code

المحلل اللغوي (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):

deoptimization.js
function add(a, b) {
  return a + b;
}

// TurboFan optimizes this function for numbers (Inline Caching)
for (let i = 0; i < 10000; i++) {
  add(5, 10);
}

// Unexpected type change:
add("5", "10"); // Deoptimization (Bailout) triggered!

يقوم المحرك بالتراجع الفوري (Bailout) وإلغاء الكود المجمع، ليعود مجدداً لتفسير الـ Bytecode الأبطأ. لهذا السبب، فإن كتابة كود أحادي الشكل (Monomorphic) وثابت البنية يُعد أحد أهم أسرار الأداء العالي.


2. ما هو سياق التنفيذ (Execution Context)؟

منذ اللحظة التي يبدأ فيها تشغيل الكود، تُدار جميع العمليات داخل بيئات معزولة تُسمى Execution Context (EC). بعبارة مبسطة، سياق التنفيذ هو "الحاوية" التي يتم فيها فحص وتشغيل الكود الحالي.

يوجد نوعان رئيسيان:

  1. سياق التنفيذ العام (GEC - Global Execution Context): ينشأ تلقائياً عند بدء تشغيل الملف. يوجد سياق عام واحد فقط يحدد الكائن العام (window في المتصفح أو global في Node.js).
  2. سياق تنفيذ الدالة (FEC - Function Execution Context): يتم إنشاؤه في كل مرة يُستدعى فيها دالة معينة.

يمر كل سياق تنفيذ بـ مرحلتين أساسيتين:

المرحلة الأولى: مرحلة الإنشاء (Creation Phase)

قبل تنفيذ أي سطر برمجي، يمسح المحرك النطاق ويقوم بحجز الذاكرة:

  • ربط الكائن العام (window أو global).
  • تحديد مرجع this الخاص بالنطاق.
  • بناء بيئة المتغيرات والبيئة المعجمية (Lexical Environment و Variable Environment).
  • تخصيص مساحات الذاكرة للمتغيرات والدوال (وهنا تحديداً يحدث مفهوم الـ Hoisting).

المرحلة الثانية: مرحلة التنفيذ (Execution Phase)

يقوم المحرك بقراءة الكود سطراً بسطر من الأعلى إلى الأسفل، وإسناد القيم الفعلية للمتغيرات واستدعاء الدوال.


3. الرفع (Hoisting) والمنطقة الميتة الزمنية (TDZ)

الرفع (Hoisting) ليس نقلاً مادياً للكود إلى أعلى الملف، بل هو نتيجة مباشرة لحجز الذاكرة أثناء Creation Phase قبل تشغيل الكود.

يكمن الاختلاف الجوهري في كيفية تهيئة التعريفات خلال هذه المرحلة:

hoisting-tdz.js
console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization

var a = 10;
let b = 20;
  • الكلمة المفتاحية 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 (ما يدخل آخراً يخرج أولاً) مساحة غير منظمة وقابلة للنمو الديناميكي
السرعة وصول مباشر وفائق السرعة أبطأ نسبياً بسبب تتبع عناوين المؤشرات
memory-allocation.js
const id = 101;                                 // Stored directly on the Stack
const user = { name: "Elgun", role: "Dev" }; // Object in Heap, address pointer on Stack

يحتفظ الـ Stack بعنوان الذاكرة فقط (0x0012FA). وعند مساواة كائن بمتغير آخر، يتم نسخ المؤشر فقط وليس بيانات الكائن الفعلية:

reference-mutation.js
const userA = { name: "Ali" };
const userB = userA; 

userB.name = "Murad";
console.log(userA.name); // "Murad" (Both pointers reference the exact same heap memory)

طفحان المكدس (Stack Overflow)

كل استدعاء لدالة يضيف إطاراً جديداً (Stack Frame) داخل الـ Call Stack، ويتم إخراجه فور انتهاء الدالة. وإذا حدث استدعاء تكراري لانهائي (Infinite Recursion)، تنفد ذاكرة المكدس ويحدث الخطأ الشهير Maximum call stack size exceeded.


5. إدارة الذاكرة: خوارزمية Mark-and-Sweep

لا يمكن للبيانات أن تتراكم في الـ Heap إلى الأبد. يقوم مجمع المهملات (Garbage Collector - GC) دورياً بالتعرف على مساحات الذاكرة غير المستخدمة وتحريرها.

تعتمد المحركات الحديثة على خوارزمية Mark-and-Sweep (التعليم والكنس):

  1. الجذور (Roots): المتغيرات العامة (Global) والمتغيرات المحلية الموجودة في إطارات المكدس النشطة تمثل نقطة البداية.
  2. التعليم (Mark): يتتبع مجمع المهملات الروابط انطلاقاً من الجذور، ويضع علامة "قابل للوصول" (Reachable) على كل كائن يصادفه، ثم يفحص تفرعاته بشكل متكرر.
  3. الكنس (Sweep): الكائنات غير القابلة للوصول تُعتبر غير مستخدمة، وتُمسح مساحتها من الذاكرة بالكامل.

خطر تسريب الذاكرة (Memory Leaks)

إذا لم تعد هناك حاجة لبيانات معينة، ولكن بقي لها مرجع مرتبط بمستمع أحداث منسي (addEventListener)، أو متغير عام غير مقصود، أو داخل دالة إغلاق (Closure) غير مغلقة، فلن يتمكن الـ GC من حذفها، مما يؤدي إلى تراكم الذاكرة وتراجع أداء المتصفح.


الخلاصة

  • محرك V8 لا يكتفي بالتفسير المباشر للكود؛ بل يحوله عبر AST ← Bytecode (بواسطة Ignition) ← كود آلة محسن (بواسطة TurboFan).
  • سياق التنفيذ (Execution Context) يدير نطاقات الكود عبر مرحلتي الإنشاء والتنفيذ. والـ Hoisting وTDZ هما نتيجة مباشرة لتصميم مرحلة الإنشاء.
  • المكدس مقابل الكومة (Stack vs Heap): تسكن البيانات الأولية وعناوين الروابط في الـ Stack فائق السرعة، بينما تعيش الكائنات المركبة في الـ Heap الديناميكي.
  • مجمع المهملات (Garbage Collector) يعتمد على مسار الوصول (Reachability) لإعادة تحرير الموارد غير المستخدمة.

بعد فهم المعمارية الداخلية للمحرك، الخطوة التالية هي استكشاف كيفية تفاعل هذه الهياكل عبر سلسلة النطاقات (Scope Chain)، والبيئة المعجمية (Lexical Environment)، ودوال الإغلاق (Closures).

مشاركة المقال

جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة

جافاسكريبت وراء الكواليس: محرك V8، سياق التنفيذ (Execution Context)، وتشريح الذاكرة | eTechmill