. أما في Node، فإمّا أن تستخدم الامتداد .mjs، أو تضبط \"type\": \"module\" داخل ملف package.json."}},{"@type":"Question","name":"ما الفرق بين default export و named export؟","acceptedAnswer":{"@type":"Answer","text":"يمكن للوحدة الواحدة أن تحتوي على عدد غير محدود من التصديرات المُسمّاة (export function foo() {})، لكن لا يُسمح إلا بتصدير افتراضي واحد فقط (export default ...). التصديرات المُسمّاة يجب استيرادها بنفس الاسم تمامًا بين أقواس معقوفة هكذا: import { foo } from './x.js'. أما التصدير الافتراضي فتستطيع استيراده بأيّ اسم تريده: import whatever from './x.js'."}},{"@type":"Question","name":"ما هو import() الديناميكي في JavaScript؟","acceptedAnswer":{"@type":"Answer","text":"عند استدعاء import() كدالة، فإنها تُرجع Promise يتحوّل إلى كائن يحتوي على صادرات الوحدة. على خلاف جملة import الثابتة، فإن import() تُنفَّذ وقت الاستدعاء، مما يسمح لك بتحميل الكود بشكل شرطي أو عند الحاجة فقط. وهذه هي الطريقة التي تُطبَّق بها تقنيات code-splitting و lazy loading."}},{"@type":"Question","name":"هل يجب كتابة امتداد الملف في مسار الاستيراد؟","acceptedAnswer":{"@type":"Answer","text":"نعم، في وحدات ES الأصلية — سواء في المتصفح أو في محمّل ESM الخاص بـ Node — يجب كتابة ./utils.js وليس ./utils. أدوات التجميع مثل Vite و webpack أكثر تساهلًا وتستطيع حلّ المسارات بدون امتداد، لكن الاعتماد على هذا يجعل كودك غير قابل للنقل."}}]}
Menu
العربية

وحدات ES في JavaScript: import و export والتحميل الديناميكي

دليل عملي لوحدات ES في JavaScript: كيفية استخدام import و export، الفرق بين التصدير الافتراضي والمُسمّى، والتحميل الديناميكي عبر import().

الوحدة (Module) هي ملف بنطاق خاص به

قبل ظهور وحدات ES في JavaScript، كان كل وسم <script> يرمي متغيراته في النطاق العام (global namespace)، وكان ترتيب التحميل هو ما يحدّد ما يراه كل سكربت. جاءت ES modules لتحلّ هذه الفوضى بجعل كل ملف نطاقاً مستقلاً بذاته؛ فلا شيء يتسرّب إلى الخارج ما لم تستخدم export صراحةً، ولا شيء يدخل إليه ما لم تستدعه عبر import صراحةً.

ملفّان، أحدهما يُصدِّر والآخر يستورد:

index.js
Output
Click Run to see the output here.

كل من add و multiply موجودان داخل ملف math.js، ولا يصبحان مرئيَين في main.js إلا عبر import. أي شيء آخر داخل math.js — من دوال مساعدة أو ثوابت أو غيرها — يبقى بعيدًا عن متناول الخارج.

من هنا تنبثق قاعدتان يجدر استيعابهما منذ البداية:

  • الوحدات (modules) تعمل تلقائيًا في الوضع الصارم (strict mode)، دون الحاجة إلى كتابة 'use strict'.
  • قيمة this في المستوى الأعلى هي undefined، وليست الكائن العام (global object).

التصدير المُسمّى (Named Exports): صدِّر أثناء الكتابة

هذا هو الأسلوب الأكثر شيوعًا. ضع كلمة export قبل أي function أو class أو const أو let، فيصبح جزءًا من الواجهة العامة للوحدة:

index.js
Output
Click Run to see the output here.

يجب أن تتطابق الأسماء بين الأقواس المعقوفة مع الأسماء المُصدَّرة تمامًا — فمثلًا import { circlearea } لن ينجح. وإذا تعارض الاسم مع شيء موجود عندك مسبقًا، يمكنك إعادة تسميته عند الاستيراد باستخدام as:

index.js
Output
Click Run to see the output here.

يمكنك أيضًا تجميع كل عمليات التصدير في أسفل الملف بدل كتابتها بجانب كل تعريف، وهي طريقة يفضّلها البعض لأنها تُبرز "الواجهة العامة" للوحدة بشكل واضح:

index.js
Output
Click Run to see the output here.

كلا الأسلوبين يعطيك نفس النتيجة في النهاية، فاختر واحداً منهما والتزم به داخل المشروع الواحد.

export default: تصدير افتراضي واحد لكل وحدة

كل وحدة تقدر تحتوي أيضاً على تصدير افتراضي (default) واحد فقط. التصدير الافتراضي مناسب للملفات اللي عندها "شيء رئيسي واحد" فعلاً — زي مكوّن، أو كلاس، أو كائن إعدادات:

index.js
Output
Click Run to see the output here.

ثلاث نقاط ينبغي الانتباه إليها:

  • لا توجد أقواس معقوفة حول log عند الاستيراد.
  • الاسم الذي تستورد به حرّ تمامًا؛ فلو كتبت import shout from './logger.js' لعمل الكود بالطريقة نفسها.
  • لا يُسمح إلا بتصدير افتراضي واحد لكل ملف. جرّب إضافة تصدير افتراضي ثانٍ وسترى أن الملف لن يُحلَّل أصلًا.

التصديرات المُسمّاة (named exports) والتصدير الافتراضي (default export) يمكن أن يتعايشا في الملف نفسه:

index.js
Output
Click Run to see the output here.

الـ default أولًا، ثم الـ named داخل الأقواس المعقوفة. الترتيب ثابت ولا يتغيّر.

أيّهما تختار؟ الـ named exports أسهل في إعادة الهيكلة — تغيير اسم ما عبر المشروع كلّه يصبح مجرد عملية بحث واستبدال واحدة، لأن كل عمليات الاستيراد تستخدم نفس الاسم. أما الـ default فهي مرنة، لكنها تتيح لكل مستورد أن يختار اسمًا مختلفًا، مما يُصعّب البحث بـ grep. معظم أدلّة الأسلوب الحديثة تميل إلى الـ named exports، وتحتفظ بالـ default للوحدات ذات الغرض الواحد فعلًا.

استيراد كل شيء، إعادة التصدير، والتأثيرات الجانبية

هناك أشكال أخرى من الاستيراد ستصادفك كثيرًا.

لجلب كل الـ named exports داخل كائن namespace واحد:

index.js
Output
Click Run to see the output here.

إعادة التصدير من وحدة أخرى دون استيرادها إلى النطاق الحالي:

index.js
Output
Click Run to see the output here.

هذه هي الطريقة التي تعتمدها المكتبات في تجميع القطع من الملفات الداخلية وعرضها من خلال واجهة عامة موحّدة.

وأخيرًا، هناك استيراد بلا أي ارتباطات — وهذا للوحدات التي لا هدف لها سوى التأثيرات الجانبية (مثل الـ polyfills، أو CSS-in-JS، أو تسجيل المعالجات):

index.js
Output
Click Run to see the output here.

الملف يُنفَّذ مرة واحدة فقط، ولا يتم استيراد أي شيء بالاسم.

الاستيرادات ساكنة وحيّة

هناك خاصيتان في import قد تفاجئان البعض من حين لآخر.

ساكنة (Static). تعليمات import تُحلّ قبل أن يبدأ تنفيذ أي سطر من شيفرتك، ولهذا لا يمكنك وضعها داخل if أو دالة أو try. كما أن المسار يجب أن يكون نصًّا حرفيًّا، لا متغيرًا. هذه القيود بالذات هي ما يسمح للأدوات بتحليل الاستيرادات دون تشغيل الكود فعليًّا — الـ bundlers ومدققات الأنواع وأدوات tree-shaking كلها تعتمد على هذه الخاصية.

// غير مسموح — SyntaxError.
if (userWantsFancy) {
  import { fancy } from './fancy.js';
}

إذا احتجت إلى تحميل مشروط، استخدم import() (سنتحدث عنها بعد قليل).

حي ومتصل. الربط المُستورَد ليس نسخة ثابتة، بل مرجع للقراءة فقط يشير إلى التصدير الأصلي. فإذا أعادت الوحدة المُصدِّرة إسناد القيمة، ستظهر القيمة الجديدة تلقائيًا لدى كل من استوردها:

index.js
Output
Click Run to see the output here.

لا يمكنك أيضاً إعادة إسناد قيمة لأي استيراد من جهة المستهلك؛ فلو كتبت count = 5 داخل main.js فسيُرمى خطأ. الاستيرادات مجرد نوافذ للقراءة فقط على ما صدّرته الوحدة.

استيراد الوحدات ديناميكياً باستخدام ()import

حين تحتاج إلى تقرير تحميل وحدة ما وقت التشغيل — كالميزات الثقيلة، أو تقسيم الشيفرة حسب المسار، أو الـ polyfills المشروطة — استخدم import() كأنها دالة. تُرجع لك وعداً (Promise) يُحَلّ إلى صادرات الوحدة:

index.js
Output
Click Run to see the output here.

بما أنه استدعاء دالة عادي، يمكنك:

  • استخدام await معه داخل دالة async.
  • تمرير متغير كمسار للملف.
  • استعماله داخل if أو try/catch.

وتفكيك الكائن الناتج يعمل تمامًا مثل الاستيراد الساكن:

index.js
Output
Click Run to see the output here.

عند تفكيك الكائن الناتج، يكون default هو المفتاح الخاص بالتصدير الافتراضي، ويمكنك إعادة تسميته لأي اسم يحلو لك.

أما الاستخدامات العملية، فأبرزها تقسيم الكود (code-splitting) — بحيث لا يتم تحميل مكتبة الرسوم البيانية مثلًا إلا حين يضغط المستخدم على "اعرض الرسم" — إضافةً إلى polyfills الخاصة باكتشاف الميزات، والإضافات (plugins) التي يتم التعرّف عليها أثناء التشغيل.

تشغيل وحدات ES في المتصفح و Node.js

الصياغة (syntax) واحدة في كل مكان، لكن ما يختلف هو الطريقة التي تبحث بها بيئة التشغيل عن الملف وتحمّله.

في المتصفح، كل ما عليك هو تعليم سكربت نقطة الدخول على أنه وحدة (module):

<script type="module" src="./main.js"></script>

مع type="module"، يتعامل المتصفح مع import/export بشكل صحيح، ويُشغّل الكود في الوضع الصارم (strict mode)، ويؤجّل تنفيذه إلى ما بعد الانتهاء من تحليل HTML. يجب أن تكون المسارات نسبية (./، ../) أو روابط URL كاملة، أما المُعرّفات المجرّدة مثل import 'lodash' فلن تعمل دون استخدام import map أو أداة تجميع (bundler).

أما في Node، فهناك طريقتان لتفعيل الوحدات:

  • تسمية الملف بامتداد .mjs، أو
  • ضبط "type": "module" في أقرب ملف package.json، وهذا يجعل كل ملف .js وحدةً (module).

كما يشترط Node ذكر المسار كاملاً مع الامتداد: import './utils.js'، وليس import './utils'.

// package.json
{
  "type": "module",
  "main": "./index.js"
}

كلتا البيئتين تتطلبان كتابة الامتداد صراحةً عند استخدام ESM الأصلي. أدوات التجميع (Vite وwebpack وesbuild) تتكفّل بحلّ المسارات بدون امتدادات أثناء التطوير — أمر مريح، لكن الاعتماد عليه يعني أن كودك المصدري لن يعمل بدون خطوة البناء.

أخطاء شائعة يقع فيها الكثيرون

إليك بعض النقاط التي تُربك المطوّرين عادةً:

  • نسيان type="module" في المتصفح. بدونه، يُعامَل <script> كسكربت تقليدي، ويصبح import خطأً نحويًا.
  • إغفال امتداد الملف في Node. فكتابة import './utils' تفشل، بينما import './utils.js' تعمل. أدوات التجميع تُخفي هذا عنك، أما بيئات التشغيل الأصلية فلا.
  • توقّع وجود __dirname أو require داخل وحدة ES. هذان خاصّان بـ CommonJS فقط. في ESM استخدم import.meta.url وحوّله عندما تحتاج إلى مسار.
  • الاستيرادات الدائرية التي تقرأ القيم قبل جاهزيتها. استيراد وحدتين لبعضهما البعض مسموح به، لكن قراءة قيمة مُصدَّرة لم تُسنَد بعد ستُعطيك undefined. رتّب كودك بحيث لا تُصطدم هذه الدائرة أثناء التهيئة، أو فكّكها إلى أجزاء.
  • محاولة استخدام import بشكل شرطي. جملة import الساكنة لا تسمح بذلك. استعمل import() الديناميكي لأي شيء يعتمد على وقت التشغيل.

التالي: الفرق بين CommonJS وESM

وحدات ES هي المعيار الحالي، لكن ما زال هناك كمّ كبير من كود Node يستخدم CommonJS — أي require وmodule.exports، مع قواعد مختلفة بشأن توقيت تنفيذ الكود. معرفة النظامين وكيفية التعامل بينهما هو موضوع الصفحة التالية.

الأسئلة الشائعة

كيف أستخدم وحدات ES في JavaScript؟

تُصدِّر القيم من ملف باستخدام export أو export default، ثم تستوردها في ملف آخر عبر import. في المتصفح، حمِّل الملف الرئيسي بهذا الشكل: <script type="module" src="main.js"></script>. أما في Node، فإمّا أن تستخدم الامتداد .mjs، أو تضبط "type": "module" داخل ملف package.json.

ما الفرق بين default export و named export؟

يمكن للوحدة الواحدة أن تحتوي على عدد غير محدود من التصديرات المُسمّاة (export function foo() {})، لكن لا يُسمح إلا بتصدير افتراضي واحد فقط (export default ...). التصديرات المُسمّاة يجب استيرادها بنفس الاسم تمامًا بين أقواس معقوفة هكذا: import { foo } from './x.js'. أما التصدير الافتراضي فتستطيع استيراده بأيّ اسم تريده: import whatever from './x.js'.

ما هو import() الديناميكي في JavaScript؟

عند استدعاء import() كدالة، فإنها تُرجع Promise يتحوّل إلى كائن يحتوي على صادرات الوحدة. على خلاف جملة import الثابتة، فإن import() تُنفَّذ وقت الاستدعاء، مما يسمح لك بتحميل الكود بشكل شرطي أو عند الحاجة فقط. وهذه هي الطريقة التي تُطبَّق بها تقنيات code-splitting و lazy loading.

هل يجب كتابة امتداد الملف في مسار الاستيراد؟

نعم، في وحدات ES الأصلية — سواء في المتصفح أو في محمّل ESM الخاص بـ Node — يجب كتابة ./utils.js وليس ./utils. أدوات التجميع مثل Vite و webpack أكثر تساهلًا وتستطيع حلّ المسارات بدون امتداد، لكن الاعتماد على هذا يجعل كودك غير قابل للنقل.

تعلّم البرمجة مع Coddy

ابدأ الآن