الوحدة (Module) هي ملف بنطاق خاص به
قبل ظهور وحدات ES في JavaScript، كان كل وسم <script> يرمي متغيراته في النطاق العام (global namespace)، وكان ترتيب التحميل هو ما يحدّد ما يراه كل سكربت. جاءت ES modules لتحلّ هذه الفوضى بجعل كل ملف نطاقاً مستقلاً بذاته؛ فلا شيء يتسرّب إلى الخارج ما لم تستخدم export صراحةً، ولا شيء يدخل إليه ما لم تستدعه عبر import صراحةً.
ملفّان، أحدهما يُصدِّر والآخر يستورد:
كل من 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، فيصبح جزءًا من الواجهة العامة للوحدة:
يجب أن تتطابق الأسماء بين الأقواس المعقوفة مع الأسماء المُصدَّرة تمامًا — فمثلًا import { circlearea } لن ينجح. وإذا تعارض الاسم مع شيء موجود عندك مسبقًا، يمكنك إعادة تسميته عند الاستيراد باستخدام as:
يمكنك أيضًا تجميع كل عمليات التصدير في أسفل الملف بدل كتابتها بجانب كل تعريف، وهي طريقة يفضّلها البعض لأنها تُبرز "الواجهة العامة" للوحدة بشكل واضح:
كلا الأسلوبين يعطيك نفس النتيجة في النهاية، فاختر واحداً منهما والتزم به داخل المشروع الواحد.
export default: تصدير افتراضي واحد لكل وحدة
كل وحدة تقدر تحتوي أيضاً على تصدير افتراضي (default) واحد فقط. التصدير الافتراضي مناسب للملفات اللي عندها "شيء رئيسي واحد" فعلاً — زي مكوّن، أو كلاس، أو كائن إعدادات:
ثلاث نقاط ينبغي الانتباه إليها:
- لا توجد أقواس معقوفة حول
logعند الاستيراد. - الاسم الذي تستورد به حرّ تمامًا؛ فلو كتبت
import shout from './logger.js'لعمل الكود بالطريقة نفسها. - لا يُسمح إلا بتصدير افتراضي واحد لكل ملف. جرّب إضافة تصدير افتراضي ثانٍ وسترى أن الملف لن يُحلَّل أصلًا.
التصديرات المُسمّاة (named exports) والتصدير الافتراضي (default export) يمكن أن يتعايشا في الملف نفسه:
الـ default أولًا، ثم الـ named داخل الأقواس المعقوفة. الترتيب ثابت ولا يتغيّر.
أيّهما تختار؟ الـ named exports أسهل في إعادة الهيكلة — تغيير اسم ما عبر المشروع كلّه يصبح مجرد عملية بحث واستبدال واحدة، لأن كل عمليات الاستيراد تستخدم نفس الاسم. أما الـ default فهي مرنة، لكنها تتيح لكل مستورد أن يختار اسمًا مختلفًا، مما يُصعّب البحث بـ grep. معظم أدلّة الأسلوب الحديثة تميل إلى الـ named exports، وتحتفظ بالـ default للوحدات ذات الغرض الواحد فعلًا.
استيراد كل شيء، إعادة التصدير، والتأثيرات الجانبية
هناك أشكال أخرى من الاستيراد ستصادفك كثيرًا.
لجلب كل الـ named exports داخل كائن namespace واحد:
إعادة التصدير من وحدة أخرى دون استيرادها إلى النطاق الحالي:
هذه هي الطريقة التي تعتمدها المكتبات في تجميع القطع من الملفات الداخلية وعرضها من خلال واجهة عامة موحّدة.
وأخيرًا، هناك استيراد بلا أي ارتباطات — وهذا للوحدات التي لا هدف لها سوى التأثيرات الجانبية (مثل الـ polyfills، أو CSS-in-JS، أو تسجيل المعالجات):
الملف يُنفَّذ مرة واحدة فقط، ولا يتم استيراد أي شيء بالاسم.
الاستيرادات ساكنة وحيّة
هناك خاصيتان في import قد تفاجئان البعض من حين لآخر.
ساكنة (Static). تعليمات import تُحلّ قبل أن يبدأ تنفيذ أي سطر من شيفرتك، ولهذا لا يمكنك وضعها داخل if أو دالة أو try. كما أن المسار يجب أن يكون نصًّا حرفيًّا، لا متغيرًا. هذه القيود بالذات هي ما يسمح للأدوات بتحليل الاستيرادات دون تشغيل الكود فعليًّا — الـ bundlers ومدققات الأنواع وأدوات tree-shaking كلها تعتمد على هذه الخاصية.
// غير مسموح — SyntaxError.
if (userWantsFancy) {
import { fancy } from './fancy.js';
}
إذا احتجت إلى تحميل مشروط، استخدم import() (سنتحدث عنها بعد قليل).
حي ومتصل. الربط المُستورَد ليس نسخة ثابتة، بل مرجع للقراءة فقط يشير إلى التصدير الأصلي. فإذا أعادت الوحدة المُصدِّرة إسناد القيمة، ستظهر القيمة الجديدة تلقائيًا لدى كل من استوردها:
لا يمكنك أيضاً إعادة إسناد قيمة لأي استيراد من جهة المستهلك؛ فلو كتبت count = 5 داخل main.js فسيُرمى خطأ. الاستيرادات مجرد نوافذ للقراءة فقط على ما صدّرته الوحدة.
استيراد الوحدات ديناميكياً باستخدام ()import
حين تحتاج إلى تقرير تحميل وحدة ما وقت التشغيل — كالميزات الثقيلة، أو تقسيم الشيفرة حسب المسار، أو الـ polyfills المشروطة — استخدم import() كأنها دالة. تُرجع لك وعداً (Promise) يُحَلّ إلى صادرات الوحدة:
بما أنه استدعاء دالة عادي، يمكنك:
- استخدام
awaitمعه داخل دالةasync. - تمرير متغير كمسار للملف.
- استعماله داخل
ifأوtry/catch.
وتفكيك الكائن الناتج يعمل تمامًا مثل الاستيراد الساكن:
عند تفكيك الكائن الناتج، يكون 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 أكثر تساهلًا وتستطيع حلّ المسارات بدون امتداد، لكن الاعتماد على هذا يجعل كودك غير قابل للنقل.