پشته‌ی فراخوانی: چگونه جاوااسکریپت مسیر اجرای توابع را دنبال می‌کند

مقدمه

در درس‌های قبل دیدیم که هر فراخوانی تابع، یک Execution Context مستقل با Lexical Environment خودش می‌سازد. اما وقتی توابع یکدیگر را صدا می‌زنند -مثلاً تابع A، تابع B را فراخوانی می‌کند و B هم تابع C را- جاوااسکریپت باید به‌خاطر بسپارد که در هر لحظه دقیقاً کدام تابع در حال اجراست و پس از پایان آن، اجرا باید به کجا برگردد. این وظیفه بر عهده‌ی ساختاری به نام Call Stack (پشته‌ی فراخوانی) است.

Call Stack چیست؟

Call Stack ساختار داده‌ای است که Execution Context ها را به ترتیب فراخوانی، روی هم انباشته می‌کند. کلمه‌ی Stack (پشته) اشاره دارد به این‌که این ساختار از الگوی LIFO پیروی می‌کند: Last In, First Out — یعنی آخرین موردی که وارد پشته شده، اولین موردی است که از آن خارج می‌شود؛ درست مانند یک پشته از بشقاب که فقط از بالا می‌توان به آن بشقاب اضافه کرد یا از آن برداشت.

نحوه‌ی عملکرد Call Stack

هر بار که تابعی فراخوانی می‌شود، Execution Context آن روی Call Stack قرار می‌گیرد (Push). وقتی تابع کارش تمام می‌شود (به return برسد یا به آخرین خط بدنه‌اش برسد)، Execution Context آن از بالای Call Stack برداشته می‌شود (Pop) و کنترل به همان‌جایی برمی‌گردد که تابع از آن‌جا فراخوانی شده بود.

مسیر توابع تودرتو روی Call Stack
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

مسیر Call Stack در این مثال به این شکل پیش می‌رود:

  1. first() فراخوانی می‌شود → Call Stack: [Global, first]
  2. داخل first، تابع second() فراخوانی می‌شود → Call Stack: [Global, first, second]
  3. داخل second، تابع third() فراخوانی می‌شود → Call Stack: [Global, first, second, third]
  4. third کارش تمام می‌شود و از Stack خارج می‌شود → Call Stack: [Global, first, second]
  5. بقیه‌ی second اجرا می‌شود، سپس از Stack خارج می‌شود → Call Stack: [Global, first]
  6. بقیه‌ی first اجرا می‌شود، سپس از Stack خارج می‌شود → Call Stack: [Global]

به همین دلیل است که ترتیب چاپ پیام‌ها به این شکل است: شروع first، شروع second، در حال اجرای third، پایان second، پایان first. تابعی که آخر از همه وارد Stack شده (third)، اولین تابعی است که کارش تمام و از Stack خارج می‌شود؛ دقیقاً همان الگوی LIFO.

Execution Flow چیست؟

Execution Flow به مسیر کلی‌ای گفته می‌شود که کد در طول زمان اجرا طی می‌کند: کدام خط، به دنبال کدام خط اجرا می‌شود، کجا وارد یک تابع می‌شویم، و کجا از آن خارج می‌شویم. Call Stack، ابزار اصلی جاوااسکریپت برای مدیریت این مسیر است؛ در واقع، در هر لحظه، بالاترین عنصر Call Stack همان چیزی است که «الان در حال اجراست».

Stack Overflow — وقتی پشته پر می‌شود

اگر توابع بدون توقف یکدیگر را فراخوانی کنند (معمولاً در اثر یک Recursion بدون شرط پایان)، Call Stack به‌طور مداوم بزرگ‌تر می‌شود تا جایی که به محدودیت حافظه‌ی مرورگر برسد و خطای RangeError: Maximum call stack size exceeded رخ دهد:

Stack Overflow در اثر فراخوانی بی‌پایان
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل رفتار جاوااسکریپت

در این مثال، callForever بدون هیچ شرط توقفی خودش را دوباره فراخوانی می‌کند؛ به همین دلیل Call Stack به‌طور پیوسته و بدون اینکه هیچ‌کدام از این فراخوانی‌ها به پایان برسند (و بنابراین هیچ‌گاه از Stack خارج شوند)، رشد می‌کند تا سرریز شود. این خطا مستقیماً نشان می‌دهد که Call Stack ظرفیت محدودی دارد؛ نه بی‌نهایت.

جاوااسکریپت تک‌رشته‌ای (Single-Threaded) است

یک نتیجه‌ی مهم وجود یک Call Stack واحد این است که جاوااسکریپت در هر لحظه فقط می‌تواند یک قطعه کد را اجرا کند؛ به این ویژگی، تک‌رشته‌ای بودن (Single-Threaded) می‌گویند. تا زمانی که تابعی در حال اجراست و از Stack خارج نشده، هیچ کد دیگری امکان اجرا شدن ندارد. این ویژگی، پایه‌ی اصلی درک نحوه‌ی مدیریت عملیات ناهمزمان (Asynchronous) در جاوااسکریپت است که در فصل جداگانه‌ای با عنوان Event Loop به‌طور کامل بررسی خواهد شد؛ در آن‌جا خواهید دید که چگونه جاوااسکریپت با وجود تک‌رشته‌ای بودن، می‌تواند کارهایی مثل تایمر یا درخواست شبکه را بدون متوقف کردن کل برنامه مدیریت کند.

جدول جمع‌بندی

مفهومتوضیح
Pushافزودن Execution Context به بالای Call Stack (هنگام فراخوانی تابع)
Popحذف Execution Context از بالای Call Stack (هنگام پایان تابع)
LIFOآخرین ورودی، اولین خروجی
Stack Overflowسرریز پشته در اثر فراخوانی بیش‌ازحد و بدون توقف

Best Practices

  • هنگام نوشتن توابع بازگشتی (Recursive)، همیشه مطمئن شوید که یک شرط پایان واضح و قابل‌دستیابی وجود دارد.
  • پیام خطای Stack Overflow را جدی بگیرید؛ معمولاً نشانه‌ی یک فراخوانی بازگشتی بدون شرط توقف مناسب است.

ارتباط با سایر مفاهیم

Call Stack، مکانیزمی است که مستقیماً روی نحوه‌ی اجرای Execution Context هایی که در درس‌های قبل دیدیم عمل می‌کند. این فصل، پایه‌ی نظری لازم برای فهم عمیق Closure (فصل بعدی) و Event Loop (در فصل مربوط به برنامه‌نویسی ناهمزمان) را کامل می‌کند؛ هر دوی این مفاهیم، مستقیماً به نحوه‌ی مدیریت Execution Context ها توسط Call Stack وابسته‌اند.

جمع‌بندی

Call Stack ساختاری است که Execution Context های در حال اجرا را به ترتیب LIFO مدیریت می‌کند؛ هر فراخوانی تابع، یک Execution Context به بالای Stack اضافه می‌کند (Push) و پایان آن تابع، همان Execution Context را از بالای Stack حذف می‌کند (Pop). فراخوانی بی‌پایان و بدون شرط توقف، Call Stack را سرریز کرده و خطای Stack Overflow تولید می‌کند. وجود یک Call Stack واحد نیز دلیل اصلی تک‌رشته‌ای بودن جاوااسکریپت است.