تا اینجای کار با نحوه نوشتن و مدیریت ماژولها آشنا شدیم. اما در پشت صحنه، مرورگر و موتور جاوااسکریپت کدهای ما را چطور پردازش میکنند؟ در این بخش قرار است با دو رفتار بسیار مهم آشنا شویم: اینکه جاوااسکریپت چطور دستورات import را بالا میکشد (Hoisting) و چطور باید با چرخه خطرناکِ فایلهایی که به همدیگر وابستهاند، دستوپنجه نرم کنیم.
یکی از ویژگیهای جالب دستورات import این است که آنها Hoist میشوند. در دنیای جاوااسکریپت، بالا کشیدن (Hoisting) یعنی موتور مفسر قبل از اجرای خط به خط کدهای شما، ابتدا به سراغ این دستورات میرود و آنها را پردازش میکند.
مقادیر وارد شده (Imported values)، حتی قبل از خطی که آنها را تعریف کردهاید در دسترس هستند!
هرگونه ساید افکت (Side effect) یا کدهای جانبی که در ماژول مقصد وجود دارد، قبل از شروع اجرای کدهای فایل فعلی شما، اجرا و تمام میشود.
به این مثال عجیب اما واقعی دقت کن؛ نوشتن دستور import در وسط کدهای فایل main.js هیچ خطایی ایجاد نمیکند و کد بدون مشکل کار میکند:
// … کدهای ابتدای فایل
// ساخت یک نمونه از کلاس Canvas قبل از خط لود کردن آن!
const myCanvas = new Canvas("myCanvas", document.body, 480, 320);
myCanvas.create();
// دستور Import در وسط کد قرار گرفته است
import { Canvas } from "./modules/canvas.js";
myCanvas.createReportList();
// … بقیه کدها
یک توصیه حرفهای (Best Practice): با وجود اینکه این کد کاملاً درست است و کار میکند، اما اصول مهندسی نرمافزار میگوید همیشه تمام دستورات
importخود را در بالاترین خطوط فایل بنویسید. این کار تحلیل وابستگیهای پروژه (Dependency Analysis) را برای خودتان و بقیه همتیمیهایتان صد برابر راحتتر میکند.
ماژولها میتوانند فایلهای دیگر را لود کنند و آنها هم به نوبه خود فایلهای دیگری را. این روابط در کنار هم یک گراف جهتدار به نام گراف وابستگی (Dependency Graph) را میسازند. در یک دنیای ایدهآل، این گراف نباید هیچ چرخهای داشته باشد (Acyclic) تا جاوااسکریپت بتواند خیلی راحت با الگوریتمهای پیمایش درخت (مثل Depth-first)، کدها را لود کند.
اما در پروژههای واقعی و پیچیده، گاهی اوقات ایجاد چرخهها غیرقابل اجتناب است. وابستگی چرخهای زمانی اتفاق میافتد که فایل a.js فایل b.js را وارد کند، اما خود فایل b.js هم به صورت مستقیم یا غیرمستقیم به فایل a.js وابسته باشد!
// Cycle:
// a.js ───> b.js
// ^ │
// └─────────┘
پاسخ کوتاه: خیر! جاوااسکریپت بسیار هوشمند است. مقدار یک متغیرِ import شده فقط زمانی واقعاً استخراج میشود که شما در کدهایتان به طور واقعی از آن استفاده کنید (به این ویژگی Live Bindings میگویند). برنامه شما تنها زمانی با خطای ReferenceError مواجه میشود که بخواهید از متغیری استفاده کنید که هنوز در فایل مقصد مقداردهی اولیه (Initialization) نشده است.
بیایید با ۳ سناریوی مختلف این موضوع را کاملاً کالبدشکافی کنیم:
اگر متغیرها به صورت ناهمگام (Asynchronous) مثلاً درون یک تگ setTimeout استفاده شوند، چرخه مشکلی ایجاد نمیکند:
// -- فایل a.js --
import { b } from "./b.js";
setTimeout(() => {
console.log(b); // خروجی: 1
}, 10);
export const a = 2;
// -- فایل b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // خروجی: 2
}, 10);
export const b = 1;
چرا این کد کار میکند؟ چون در زمان لود شدن اولیه ماژولها، هیچکدام از فایلها فوراً مقدار a یا b را نمیخوانند. ابتدا هر دو فایل به طور کامل لود و مقادیر صادر شده ساخته میشوند. سپس بعد از ۱۰ میلیثانیه، توابع اسینکرون اجرا میشوند و چون در آن زمان هر دو متغیر در حافظه وجود دارند، همهچیز عالی کار میکند!
اگر بخواهید بلافاصله و به صورت همگام (Synchronous) از متغیر فایل چرخهای استفاده کنید، برنامه با مغز به زمین میخورد:
// -- فایل a.js (فایل اصلی ورودی) --
import { b } from "./b.js";
export const a = 2;
// -- فایل b.js --
import { a } from "./a.js";
// ❌ خطا! تلاش برای دسترسی همگام به متغیری که هنوز ساخته نشده است
console.log(a); // ReferenceError: Cannot access 'a' before initialization
export const b = 1;
چرا این کد خطا میدهد؟ وقتی جاوااسکریپت میخواهد a.js را اجرا کند، میبیند به b.js نیاز دارد، پس رفتن به سراغ a را متوقف کرده و شروع به پردازش b.js میکند. اما داخل b.js ناگهان دستور console.log(a) ظاهر میشود! در این لحظه متغیر a هنوز در فایل اول مقداردهی نشده است، در نتیجه با خطای عدم دسترسی مواجه میشوید.
اگر یکی از فایلها از متغیر به صورت همگام و دیگری به صورت ناهمگام استفاده کند، داستان چطور میشود؟
// -- فایل a.js (فایل اصلی ورودی) --
import { b } from "./b.js";
console.log(b); // خروجی: 1
export const a = 2;
// -- فایل b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // خروجی: 2
}, 10);
export const b = 1;
چرا این کد کار میکند؟ چون روند ارزیابی فایل b.js بدون نیاز فوری به a به طور کامل و موفقیتآمیز تمام میشود، پس مقدار b (یعنی عدد ۱) آماده میشود. حالا نوبت به ادامه اجرای a.js میرسد و چون b کاملاً آماده است، دستور console.log(b) بدون مشکل کارش را انجام میدهد!
از آنجا که وجود چرخهها کد شما را به شدت حساس، خطرناک و مستعد خطا (Error-prone) میکند، همیشه توصیه میشود تا جایی که میتوانید از آنها دوری کنید. ۴ تکنیک طلایی برای حذف چرخهها عبارتند از:
۱. ادغام فایلها: دو ماژولِ وابسته را ترکیب کرده و هر دو را تبدیل به یک فایل واحد کنید. ۲. فایل واسط سوم: کدهای مشترکی را که هر دو فایل به آن نیاز دارند، به یک فایل سوم (مثلاً shared.js) منتقل کنید تا هر دو از آن فایلِ مستقل خط بگیرند. ۳. جابهجایی منطق کد: بخشی از کدهای کلیدی را از یک ماژول به ماژول دیگر منتقل کنید تا رابطه دوطرفه قطع شود.
نکته پایانی: گاهی اوقات این چرخهها به خاطر وابستگی دو کتابخانه خارجی (Libraries) به یکدیگر رخ میدهند که در این حالت، حل کردن آن بسیار سختتر است و نیاز به بررسی دقیقتر مستندات آن ابزارها دارد.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://developer.mozilla.org/en-US/docs/Web/JavaScript