جافاسكريبت الحديثة: دليل شامل للمفاهيم الأساسية في ES6+ (Imports, Exports, Let, Const, Promises)
شهدت لغة جافاسكريبت (JavaScript) تطورات وتحسينات جذرية على مدار السنوات القليلة الماضية، مما جعلها أكثر قوة ومرونة. هذه التحديثات لا غنى عنها لأي مطور يسعى لتحسين جودة شيفرته البرمجية، ومواكبة أحدث التوجهات التقنية، بل وحتى تعزيز مسيرته المهنية. إن فهم الميزات الجديدة أمر بالغ الأهمية، خاصة عند العمل مع مكتبات جافاسكريبت الشهيرة مثل React أو أطر العمل مثل Angular وVue.
من بين الإضافات القيمة التي طرأت على اللغة مؤخرًا، نجد عامل الدمج الصفري (Nullish coalescing operator)، وسلسلة الاختيار الاختياري (optional chaining)، والوعود (Promises)، والدوال غير المتزامنة (async/await)، وتفكيك ES6 (ES6 destructuring)، وغيرها الكثير. في هذا المقال، سنتعمق في استكشاف بعض هذه المفاهيم الأساسية التي يجب على كل مطور جافاسكريبت إتقانها. لنبدأ رحلتنا في عالم جافاسكريبت الحديثة.
let وconst في جافاسكريبت: إدارة النطاق بفعالية
قبل ظهور معيار ES6، كانت جافاسكريبت تعتمد بشكل أساسي على الكلمة المفتاحية var لتعريف المتغيرات، والتي كانت تقتصر في نطاقها على مستوى الدالة (function scope) أو النطاق العام (global scope). لم يكن هناك مفهوم للنطاق على مستوى الكتلة البرمجية (block-level scope). مع إدخال let وconst، أضافت جافاسكريبت دعمًا قويًا لنطاق الكتلة، مما أحدث ثورة في كيفية إدارة المتغيرات وتجنب الأخطاء المحتملة.
كيفية استخدام let في جافاسكريبت
عند تعريف متغير باستخدام الكلمة المفتاحية let، يمكننا إعادة تعيين قيمة جديدة لهذا المتغير لاحقًا، ولكن لا يمكننا إعادة تعريفه بنفس الاسم في نفس النطاق. لنلقِ نظرة على مثال يوضح الفرق بين var وlet:
// شيفرة ES5
var value = 10;
console.log(value); // 10
var value = "hello";
console.log(value); // hello
var value = 30;
console.log(value); // 30
كما يتضح من الشيفرة أعلاه، قمنا بإعادة تعريف المتغير value عدة مرات باستخدام الكلمة المفتاحية var. قبل ES6، كان هذا السلوك مسموحًا به، ولكنه كان غالبًا ما يؤدي إلى تداخل في الأسماء وأخطاء يصعب تتبعها، خاصة إذا تم إعادة تعريف متغير موجود دون قصد، مما قد يغير قيمته بشكل غير متوقع.
باستخدام let، يتم منع هذه المشكلة. عند محاولة إعادة تعريف متغير بنفس الاسم في نفس النطاق، ستتلقى خطأ في بناء الجملة (SyntaxError)، وهو ما يُعد ميزة أمان مهمة:
// شيفرة ES6
let value = 10;
console.log(value); // 10
let value = "hello"; // Uncaught SyntaxError: Identifier 'value' has already been declared
ومع ذلك، فإن إعادة تعيين قيمة جديدة لمتغير تم تعريفه بـlet أمر مسموح به تمامًا:
// شيفرة ES6
let value = 10;
console.log(value); // 10
value = "hello";
console.log(value); // hello
في المثال أعلاه، لم نحصل على خطأ لأننا قمنا بإعادة تعيين قيمة للمتغير value، وليس إعادة تعريفه.
نطاق الكتلة (Block Scope) مع let
الفرق الجوهري الآخر بين var وlet يكمن في كيفية تعاملهما مع النطاق. لنقارن سلوك var داخل كتلة if:
// شيفرة ES5
var isValid = true;
if (isValid) {
var number = 10;
console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10
كما نرى، المتغير number المُعرّف بـvar داخل كتلة if يظل متاحًا خارج هذه الكتلة أيضًا. هذا السلوك قد يؤدي إلى نتائج غير متوقعة.
الآن، لنرى كيف تتصرف let في نفس السيناريو:
// شيفرة ES6
let isValid = true;
if (isValid) {
let number = 10;
console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // Uncaught ReferenceError: number is not defined
هنا، المتغير number المُعرّف باستخدام let يكون متاحًا فقط داخل كتلة if. عند محاولة الوصول إليه خارج الكتلة، نحصل على خطأ مرجعي (ReferenceError)، مما يؤكد مفهوم نطاق الكتلة.
إذا كان هناك متغير number مُعرّف خارج كتلة if، فسيتم التعامل معهما كمتغيرين منفصلين في نطاقين مختلفين:
// شيفرة ES6
let isValid = true;
let number = 20;
if (isValid) {
let number = 10;
console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 20
في هذا المثال، لدينا متغيران باسم number في نطاقين منفصلين. لذا، خارج كتلة if، تظل قيمة number هي 20.
let وfor الحلقات التكرارية
ينطبق نفس المبدأ على الحلقات التكرارية. لنقارن var مع let في حلقة for:
// شيفرة ES5
for (var i = 0; i < 10; i++){
console.log(i);
}
console.log('outside:', i); // 10
باستخدام var، يظل المتغير i متاحًا حتى خارج حلقة for.
// شيفرة ES6
for (let i = 0; i < 10; i++){
console.log(i);
}
console.log('outside:', i); // Uncaught ReferenceError: i is not defined
ولكن عند استخدام let، لا يكون المتغير i متاحًا خارج الحلقة، مما يمنع التداخل غير المرغوب فيه. هذا يوضح أن let يجعل المتغير متاحًا فقط داخل الكتلة التي تم تعريفه فيها.
يمكننا أيضًا إنشاء كتلة برمجية باستخدام زوج من الأقواس المعقوفة {}:
let i = 10;
{
let i = 20;
console.log('inside:', i); // inside: 20
i = 30;
console.log('i again:', i); // i again: 30
}
console.log('outside:', i); // outside: 10
في هذا المثال، قمنا بإعادة تعريف i بقيمة 20 داخل الكتلة. هذه القيمة تكون متاحة فقط داخل تلك الكتلة. عندما قمنا بطباعة المتغير i خارج الكتلة، حصلنا على القيمة 10 بدلًا من 30، لأن المتغير i الداخلي لا وجود له خارج نطاقه. إذا لم يكن المتغير i مُعرّفًا خارج الكتلة، فسنحصل على خطأ:
{
let i = 20;
console.log('inside:', i); // inside: 20
i = 30;
console.log('i again:', i); // i again: 30
}
console.log('outside:', i); // Uncaught ReferenceError: i is not defined
كيفية استخدام const في جافاسكريبت
تعمل الكلمة المفتاحية const بنفس طريقة let فيما يتعلق بوظيفة نطاق الكتلة. ومع ذلك، يكمن الاختلاف الجوهري بينهما في طبيعة القيمة التي يمكن أن يحملها المتغير. عند تعريف متغير باستخدام const، يُعتبر هذا المتغير ثابتًا، أي أن قيمته لا ينبغي أن تتغير بعد التعيين الأولي.
على عكس let، حيث يمكننا إعادة تعيين قيمة جديدة للمتغير لاحقًا:
let number = 10;
number = 20;
console.log(number); // 20
لا يمكننا فعل ذلك مع const. محاولة إعادة تعيين قيمة لمتغير const ستؤدي إلى خطأ من نوع TypeError:
const number = 10;
number = 20; // Uncaught TypeError: Assignment to constant variable.
كما لا يمكننا إعادة تعريف متغير const بنفس الاسم في نفس النطاق، تمامًا كما هو الحال مع let:
const number = 20;
console.log(number); // 20
const number = 10; // Uncaught SyntaxError: Identifier 'number' has already been declared
const مع الأنواع المرجعية (Reference Types)
هناك نقطة مهمة يجب فهمها عند استخدام const مع الأنواع المرجعية (مثل المصفوفات والكائنات). لننظر إلى المثال التالي:
const arr = [1, 2, 3, 4];
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]
قد يبدو هذا متناقضًا، فكيف يمكن لمصفوفة مُعرّفة كـconst أن تتغير؟ التفسير يكمن في أن المصفوفات هي أنواع مرجعية وليست أنواعًا بدائية (primitive types) في جافاسكريبت. ما يتم تخزينه فعليًا في المتغير arr ليس المصفوفة بحد ذاتها، بل هو مرجع (عنوان) لموقع الذاكرة حيث يتم تخزين المصفوفة الحقيقية.
لذلك، عند تنفيذ arr.push(5);، نحن لا نغير المرجع الذي يشير إليه arr، بل نغير القيم المخزنة في موقع الذاكرة الذي يشير إليه هذا المرجع. ينطبق نفس المبدأ على الكائنات (objects):
const obj = { name: 'David', age: 30 };
obj.age = 40;
console.log(obj); // { name: 'David', age: 40 }
هنا أيضًا، نحن لا نغير مرجع الكائن obj، بل نعدّل القيم المخزنة في المرجع الحالي. لذلك، ستعمل الشيفرة أعلاه بشكل صحيح.
ولكن، محاولة تغيير المرجع نفسه ستؤدي إلى خطأ:
const obj = { name: 'David', age: 30 };
const obj1 = { name: 'Mike', age: 40 };
obj = obj1; // Uncaught TypeError: Assignment to constant variable.
هذه الشيفرة لا تعمل لأننا نحاول تغيير المرجع الذي يشير إليه المتغير const. النقطة الأساسية التي يجب تذكرها عند استخدام const هي أنه لا يمكننا إعادة تعريف المتغير أو إعادة تعيينه لمرجع جديد. ومع ذلك، يمكننا تغيير القيم المخزنة في موقع الذاكرة الذي يشير إليه المتغير إذا كان من نوع مرجعي (مثل مصفوفة أو كائن).
لذلك، الشيفرة التالية غير صالحة لأنها تحاول إعادة تعيين قيمة جديدة للمصفوفة بأكملها (أي تغيير المرجع):
const arr = [1, 2, 3, 4];
arr = [10, 20, 30]; // Uncaught TypeError: Assignment to constant variable.
ولكن، كما رأينا سابقًا، يمكننا تغيير القيم داخل المصفوفة. وبالمثل، فإن إعادة تعريف متغير const غير مسموح به:
const name = "David";
const name = "Raj"; // Uncaught SyntaxError: Identifier 'name' has already been declared
ملخص let وconst
تضيف الكلمتان المفتاحيتان let وconst مفهوم نطاق الكتلة (block scoping) إلى جافاسكريبت، مما يوفر تحكمًا أفضل في المتغيرات ويقلل من الأخطاء المحتملة:
- عند تعريف متغير باستخدام
let: لا يمكننا إعادة تعريفه أو إعادة الإعلان عنه بنفس الاسم في نفس النطاق (سواء كان نطاق دالة أو نطاق كتلة)، ولكن يمكننا إعادة تعيين قيمة جديدة له. - عند تعريف متغير باستخدام
const: لا يمكننا إعادة تعريفه أو إعادة الإعلان عنه بنفس الاسم في نفس النطاق. بالإضافة إلى ذلك، لا يمكننا إعادة تعيينه لمرجع جديد. ومع ذلك، يمكننا تغيير القيم المخزنة داخل المتغير إذا كان من نوع مرجعي، مثل مصفوفة (array) أو كائن (object).
الآن، لننتقل إلى موضوع آخر مهم للغاية: الوعود (Promises).
الوعود (Promises) في جافاسكريبت: إدارة العمليات غير المتزامنة
تُعد الوعود (Promises) من أهم المفاهيم في جافاسكريبت الحديثة، وإن كانت غالبًا ما تكون مصدر حيرة وصعوبة في الفهم لكل من المطورين الجدد وذوي الخبرة. تم إدخال الوعود كجزء أساسي من معيار ES6، لتوفير طريقة أصلية وفعالة للتعامل مع العمليات غير المتزامنة.
فما هو الوعد (Promise)؟ ببساطة، يمثل الوعد عملية غير متزامنة من المتوقع اكتمالها في المستقبل. قبل ES6، كانت إدارة العمليات غير المتزامنة، مثل استدعاءات واجهة برمجة التطبيقات (API calls)، تتطلب الاعتماد على مكتبات خارجية مثل jQuery أو Ajax، التي كانت توفر تطبيقاتها الخاصة للوعود. لم يكن هناك دعم أصيل للوعود في المتصفحات.
ولكن الآن، بفضل الوعود في ES6، أصبح بإمكاننا إجراء استدعاءات API والانتظار حتى اكتمالها لتنفيذ عمليات معينة بشكل سلس ومنظم، مما يسهل بناء تطبيقات ويب أكثر استجابة وفعالية.
كيفية إنشاء وعد (Promise)
لإنشاء وعد، نستخدم دالة البناء Promise على النحو التالي:
const promise = new Promise(function (resolve, reject) {
// الكود غير المتزامن هنا
});
تأخذ دالة البناء Promise دالة كوسيط، وتتلقى هذه الدالة داخليًا وسيطين هما resolve وreject. هاتان الوظيفتان هما في الواقع دوال يمكننا استدعاؤها بناءً على نتيجة العملية غير المتزامنة.
يمر الوعد بثلاث حالات رئيسية:
Pending(معلق): الحالة الأولية للوعد، قبل اكتمال العملية أو فشلها.Fulfilled(مكتمل/ناجح): عندما يتم استدعاء الدالةresolve، مما يشير إلى اكتمال العملية بنجاح.Rejected(مرفوض/فاشل): عندما يتم استدعاء الدالةreject، مما يشير إلى فشل العملية.
لمحاكاة عملية طويلة الأمد أو غير متزامنة، سنستخدم الدالة setTimeout:
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = 4 + 5;
resolve(sum); // يتم حل الوعد بقيمة المجموع
}, 2000); // بعد ثانيتين
});
في هذا المثال، أنشأنا وعدًا سيتم حله بقيمة مجموع 4 و5 بعد انتهاء مهلة 2000ms (ثانيتين). للحصول على نتيجة التنفيذ الناجح للوعد، نحتاج إلى تسجيل دالة استدعاء (callback) باستخدام المعالج .then:
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = 4 + 5;
resolve(sum);
}, 2000);
});
promise.then(function (result) {
console.log(result); // 9
});
عند استدعاء resolve، يُعيد الوعد القيمة التي تم تمريرها إلى دالة resolve، والتي يمكننا التقاطها باستخدام معالج .then.
أما إذا كانت العملية غير ناجحة، فإننا نستدعي الدالة reject:
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = 4 + 5 + 'a'; // ستجعل المجموع غير رقمي
if (isNaN(sum)) { // التحقق مما إذا كان المجموع ليس رقمًا
reject('Error while calculating sum.'); // رفض الوعد مع رسالة خطأ
} else {
resolve(sum); // حل الوعد بقيمة المجموع
}
}, 2000);
});
promise.then(function (result) {
console.log(result);
});
هنا، إذا لم يكن sum رقمًا، فإننا نستدعي دالة reject مع رسالة الخطأ. وإلا، فإننا نستدعي دالة resolve. عند تنفيذ الشيفرة أعلاه بدون معالج للأخطاء، ستلاحظ مخرجات مشابهة لما يلي:

كما يتضح، نحصل على رسالة خطأ غير معالجة (uncaught error) بالإضافة إلى الرسالة التي حددناها، لأن استدعاء دالة reject يثير خطأ. لمنع توقف التطبيق بشكل مفاجئ والتعامل مع الأخطاء بشكل صحيح، يجب إضافة معالج للأخطاء باستخدام .catch:
promise.then(function (result) {
console.log(result);
}).catch(function (error) {
console.log(error); // طباعة رسالة الخطأ
});
عند إضافة معالج .catch، ستكون المخرجات كما يلي:

بإضافة معالج .catch، نتجنب ظهور الأخطاء غير المعالجة ونقوم فقط بتسجيل الخطأ في وحدة التحكم، مما يمنع توقف التطبيق فجأة. لذلك، يُنصح دائمًا بإضافة معالج .catch لكل وعد لضمان استمرارية عمل التطبيق حتى في حالة حدوث أخطاء.
سلسلة الوعود (Promise Chaining)
تتيح لنا الوعود إمكانية ربط عدة معالجات .then ببعضها البعض، مما يسمح بتنفيذ سلسلة من العمليات المتتالية بشكل منظم. يُعرف هذا النمط بسلسلة الوعود (Promise Chaining). يمكننا إضافة معالجات .then متعددة لوعد واحد على النحو التالي:
promise.then(function (result) {
console.log('first .then handler'); // المعالج الأول
return result; // تمرير النتيجة إلى المعالج التالي
}).then(function (result) {
console.log('second .then handler'); // المعالج الثاني
console.log(result); // طباعة النتيجة المستلمة
}).catch(function (error) {
console.log(error); // معالجة أي أخطاء في السلسلة
});
عند إضافة معالجات .then متعددة، يتم تمرير القيمة المُعادة من معالج .then السابق تلقائيًا إلى معالج .then التالي. لنلقِ نظرة على المخرجات المتوقعة:

كما يظهر، عند حل الوعد بقيمة 4 + 5، نحصل على هذا المجموع في معالج .then الأول. هناك، نقوم بطباعة رسالة ثم نعيد المجموع إلى معالج .then التالي. وداخل المعالج الثاني، نقوم بإضافة رسالة أخرى ثم نطبع النتيجة التي استلمناها من المعالج السابق. هذه الطريقة في إضافة معالجات .then متعددة هي ما يُعرف بسلسلة الوعود، وهي أداة قوية لتنظيم الشيفرة غير المتزامنة.
تأخير تنفيذ الوعد في جافاسكريبت
في كثير من الأحيان، قد لا نرغب في إنشاء الوعد وتنفيذه فورًا، بل نفضل تأخير إنشائه حتى اكتمال عملية معينة. لتحقيق ذلك، يمكننا تغليف الوعد داخل دالة وإعادته من تلك الدالة:
function createPromise() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = 4 + 5;
if (isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
}
بهذه الطريقة، يمكننا استخدام معاملات الدالة داخل الوعد، مما يجعل الدالة ديناميكية حقًا:
function createPromise(a, b) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = a + b;
if (isNaN(sum)) {
reject('Error while calculating sum.');
} else {
resolve(sum);
}
}, 2000);
});
}
createPromise(1, 8)
.then(function (output) {
console.log(output); // 9
});
// أو
createPromise(10, 24)
.then(function (output) {
console.log(output); // 34
});

ملاحظات هامة حول الوعود:
- عند إنشاء وعد، فإنه إما أن يتم حله (
resolved) أو رفضه (rejected)، ولا يمكن أن يحدث الاثنان في نفس الوقت. لذلك، لا يمكننا استدعاء دالتيresolveوrejectفي نفس الوعد. - يمكننا تمرير قيمة واحدة فقط إلى دالتي
resolveأوreject. إذا كنت ترغب في تمرير قيم متعددة إلى دالةresolve، يمكنك تمريرها ككائن (object) على النحو التالي:
const promise = new Promise(function (resolve, reject) {
setTimeout(function () {
const sum = 4 + 5;
resolve({ a: 4, b: 5, sum }); // تمرير كائن يحتوي على قيم متعددة
}, 2000);
});
promise.then(function (result) {
console.log(result); // { a: 4, b: 5, sum: 9 }
}).catch(function (error) {
console.log(error);
});

استخدام دوال الأسهم (Arrow Functions) في جافاسكريبت
في جميع الأمثلة السابقة، استخدمنا بناء الجملة التقليدي للدوال في ES5 عند إنشاء الوعود. ومع ذلك، أصبح من الشائع جدًا استخدام بناء جملة دوال الأسهم (Arrow Functions) في ES6+، لما توفره من إيجاز ووضوح، خاصة عند استخدامها كدوال استدعاء (callbacks). إليك نفس المثال السابق باستخدام دوال الأسهم:
const promise = new Promise( (resolve, reject) => {
setTimeout( () => {
const sum = 4 + 5 + 'a';
if (isNaN(sum)) {
reject('Error while calculating sum.');
}
else {
resolve(sum);
}
}, 2000);
});
promise.then( (result) => {
console.log(result);
});
يمكنك اختيار استخدام بناء جملة دوال ES5 أو ES6 (دوال الأسهم) بناءً على تفضيلاتك ومتطلبات مشروعك، مع الأخذ في الاعتبار أن دوال الأسهم توفر أيضًا سلوكًا مختلفًا لـthis، وهو ما يتجاوز نطاق هذا المقال.
بناء جملة الاستيراد والتصدير (Import and Export) في ES6+
قبل ظهور ES6، كانت مشاريع جافاسكريبت تعتمد على تضمين عدة ملفات .js باستخدام وسوم <script> في ملف HTML واحد، على النحو التالي:
<script type="text/javascript" src="home.js"></script>
<script type="text/javascript" src="profile.js"></script>
<script type="text/javascript" src="user.js"></script>
كان هذا النهج يسبب مشكلة رئيسية: إذا كان لدينا متغير بنفس الاسم في ملفات جافاسكريبت مختلفة، فإنه سيؤدي إلى تضارب في الأسماء (naming conflict)، وقد تحصل على قيمة غير متوقعة. لقد عالج ES6 هذه المشكلة من خلال تقديم مفهوم الوحدات (modules).
في ES6، يُعرف كل ملف جافاسكريبت نكتبه كوحدة (module). المتغيرات والدوال التي نعلنها داخل كل ملف لا تكون متاحة للملفات الأخرى إلا إذا قمنا بتصديرها (export) صراحة من هذا الملف واستيرادها (import) في ملف آخر. هذا يعني أن الدوال والمتغيرات المعرفة في الملف تكون خاصة به ولا يمكن الوصول إليها من الخارج إلا بعد تصديرها.
هناك نوعان رئيسيان من عمليات التصدير:
- التصدير المسمى (
Named Exports): يمكن أن يكون هناك عدة تصديرات مسماة في ملف واحد. - التصدير الافتراضي (
Default Exports): يمكن أن يكون هناك تصدير افتراضي واحد فقط في ملف واحد.
التصدير المسمى (Named Exports) في جافاسكريبت
يُستخدم التصدير المسمى لتصدير عدة قيم من ملف واحد. لتصدير قيمة واحدة كـNamed Export، يمكننا القيام بذلك مباشرة عند تعريفها:
export const temp = "This is some dummy text";
إذا كان لدينا عدة عناصر لتصديرها، يمكننا كتابة عبارة export في سطر منفصل بعد تعريف المتغيرات. نحدد العناصر المراد تصديرها داخل أقواس معقوفة {}:
const temp1 = "This is some dummy text1";
const temp2 = "This is some dummy text2";
export { temp1, temp2 };
ملاحظة هامة: بناء جملة التصدير هذا ليس بناء جملة حرفي لكائن (object literal syntax). لذلك، في ES6، لا يمكننا تصدير العناصر باستخدام أزواج المفتاح والقيمة مباشرة بهذه الطريقة:
// بناء جملة تصدير غير صالح في ES6
export { key1 : value1, key2 : value2 } // سيؤدي إلى خطأ
استيراد التصديرات المسماة
لاستيراد العناصر التي قمنا بتصديرها كـNamed Export، نستخدم بناء الجملة التالي:
import { temp1, temp2 } from './filename';
تجدر الإشارة إلى أنه عند استيراد شيء من ملف، لا نحتاج إلى إضافة امتداد .js إلى اسم الملف، حيث يتم اعتباره افتراضيًا.
// استيراد من ملف functions.js من الدليل الحالي
import { temp1, temp2 } from './functions';
// استيراد من ملف functions.js من الدليل الأب للدليل الحالي
import { temp1 } from '../functions';
يمكنك تجربة هذا المفهوم في هذا العرض التوضيحي على Code Sandbox: https://codesandbox.io/s/hardcore-pond-q4cjx
من المهم ملاحظة أن الاسم المستخدم أثناء التصدير يجب أن يتطابق مع الاسم الذي نستخدمه أثناء الاستيراد. على سبيل المثال، إذا قمت بالتصدير كالتالي:
// constants.js
export const PI = 3.14159;
فيجب عليك استخدام نفس الاسم PI عند الاستيراد:
import { PI } from './constants';
لا يمكنك استخدام اسم آخر مثل هذا:
import { PiValue } from './constants'; // سيؤدي هذا إلى خطأ
ولكن إذا كان لديك بالفعل متغير بنفس اسم المتغير المُصدَّر، يمكنك استخدام بناء جملة إعادة التسمية (renaming syntax) أثناء الاستيراد:
import { PI as PIValue } from './constants';
هنا، قمنا بإعادة تسمية PI إلى PIValue، وبالتالي لا يمكننا استخدام اسم المتغير PI الآن. بدلاً من ذلك، يجب علينا استخدام المتغير PIValue للحصول على القيمة المصدرة لـPI. يمكننا أيضًا استخدام بناء جملة إعادة التسمية وقت التصدير:
// constants.js
const PI = 3.14159;
export { PI as PIValue };
ثم عند الاستيراد، يجب علينا استخدام PIValue بهذه الطريقة:
import { PIValue } from './constants';
لتصدير شيء كـNamed Export، يجب علينا تعريفه أولاً. الأمثلة التالية توضح ذلك:
export 'hello';// هذا سيؤدي إلى خطأ.export const greeting = 'hello';// هذا سيعمل بشكل صحيح.export { name: 'David' };// هذا سيؤدي إلى خطأ.export const object = { name: 'David' };// هذا سيعمل بشكل صحيح.
الترتيب الذي نستورد به التصديرات المسماة المتعددة ليس مهمًا. لنلقِ نظرة على ملف validations.js التالي:
// utils/validations.js
const isValidEmail = function (email) {
if (/^[^@ ]+@[^@ ]+\.[^@ \.]{2,}$/.test(email)) {
return "email is valid";
} else {
return "email is invalid";
}
};
const isValidPhone = function (phone) {
if (/^[\\(]\d{3}[\\)]\s\d{3}-\d{4}$/.test(phone)) {
return "phone number is valid";
} else {
return "phone number is invalid";
}
};
function isEmpty(value) {
if (/^\s*$/.test(value)) {
return "string is empty or contains only spaces";
} else {
return "string is not empty and does not contain spaces";
}
// يمكن تبسيط هذه الدالة باستخدام value.trim().length === 0
}
export { isValidEmail, isValidPhone, isEmpty };
وفي ملف index.js، نستخدم هذه الدوال كما هو موضح أدناه:
// index.js
import { isEmpty, isValidEmail } from "./utils/validations";
console.log("isEmpty:", isEmpty("abcd")); // isEmpty: string is not empty and does not contain spaces
console.log("isValidEmail:", isValidEmail("abc@11gmail.com")); // isValidEmail: email is valid
console.log("isValidEmail:", isValidEmail("ab@c@11gmail.com")); // isValidEmail: email is invalid
يمكنك مشاهدة عرض توضيحي لهذه الشيفرة هنا: https://codesandbox.io/s/youthful-flower-xesus. كما ترى، يمكننا استيراد العناصر المصدرة المطلوبة فقط وبأي ترتيب، مما يلغي الحاجة إلى التحقق من ترتيب التصدير في الملف الآخر. هذه هي إحدى جماليات التصديرات المسماة.
التصدير الافتراضي (Default Exports) في جافاسكريبت
كما ذكرنا سابقًا، يمكن أن يكون هناك تصدير افتراضي واحد فقط في الملف الواحد. ومع ذلك، يمكن دمج عدة تصديرات مسماة مع تصدير افتراضي واحد في نفس الملف. للإعلان عن تصدير افتراضي، نضيف الكلمة المفتاحية default قبل الكلمة المفتاحية export:
// constants.js
const name = 'David';
export default name;
لاستيراد التصدير الافتراضي، لا نستخدم الأقواس المعقوفة {} كما فعلنا مع التصديرات المسماة:
import name from './constants';
إذا كان لدينا عدة تصديرات مسماة وتصدير افتراضي واحد، يمكننا استيرادها كلها في سطر واحد. يجب أن يأتي المتغير المُصدَّر افتراضيًا قبل الأقواس المعقوفة التي تحتوي على التصديرات المسماة:
// constants.js
export const PI = 3.14159;
export const AGE = 30;
const NAME = "David";
export default NAME;
ثم نستوردها كالتالي:
// هنا، NAME هو التصدير الافتراضي، و PI و AGE هما تصديران مسميان
import NAME, { PI, AGE } from './constants';
مرونة التصدير الافتراضي
إحدى الميزات الفريدة للتصدير الافتراضي هي أنه يمكننا تغيير اسم المتغير المُصدَّر أثناء الاستيراد:
// constants.js
const AGE = 30;
export default AGE;
وفي ملف آخر، يمكننا استخدام اسم مختلف عند الاستيراد:
import myAge from './constants';
console.log(myAge); // 30
هنا، قمنا بتغيير اسم المتغير المُصدَّر افتراضيًا من AGE إلى myAge. يعمل هذا لأن هناك تصدير افتراضي واحد فقط لكل ملف، لذا يمكنك تسميته بأي اسم تريده عند الاستيراد.
نقطة أخرى يجب ملاحظتها هي أن الكلمة المفتاحية export default لا يمكن أن تأتي قبل تعريف المتغير مباشرة بهذا الشكل:
// constants.js
export default const AGE = 30; // هذا خطأ ولن يعمل
بدلاً من ذلك، يجب استخدام الكلمة المفتاحية export default في سطر منفصل بعد تعريف المتغير:
// constants.js
const AGE = 30;
export default AGE;
ومع ذلك، يمكننا تصدير قيمة افتراضية دون تعريفها كمتغير أولاً، وذلك بتصدير كائن أو دالة مباشرة:
// constants.js
export default { name: "Billy", age: 40 };
وفي ملف آخر، نستخدمه بهذه الطريقة:
import user from './constants';
console.log(user.name); // Billy
console.log(user.age); // 40
استيراد جميع التصديرات
توجد طريقة أخرى لاستيراد جميع المتغيرات المصدرة في ملف باستخدام بناء الجملة التالي:
import * as constants from './constants';
هنا، نقوم باستيراد جميع التصديرات المسماة والافتراضية الموجودة في ملف constants.js وتخزينها في المتغير constants. سيصبح constants كائنًا يحتوي على جميع التصديرات:
// constants.js
export const USERNAME = "David";
export default { name: "Billy", age: 40 };
وفي ملف آخر، نستخدمه كما يلي:
// test.js
import * as constants from './constants';
console.log(constants.USERNAME); // David
console.log(constants.default); // { name: "Billy", age: 40 }
console.log(constants.default.age); // 40
يمكنك رؤية عرض توضيحي لهذه الوظيفة هنا: https://codesandbox.io/s/green-hill-dj43b
إذا كنت لا ترغب في فصل التصديرات الافتراضية والمسماة في أسطر منفصلة، يمكنك دمجها كما هو موضح أدناه:
// constants.js
const PI = 3.14159;
const AGE = 30;
const USERNAME = "David";
const USER = { name: "Billy", age: 40 };
export { PI, AGE, USERNAME, USER as default };
هنا، نقوم بتصدير USER كتصدير افتراضي، والبقية كتصديرات مسماة. في ملف آخر، يمكنك استخدامها بهذه الطريقة:
import USER, { PI, AGE, USERNAME } from "./constants";
عرض توضيحي لهذا الدمج متاح هنا: https://codesandbox.io/s/eloquent-northcutt-7btp1
باختصار: في ES6، لا يمكن الوصول إلى البيانات المُعلنة في ملف واحد من ملف آخر إلا إذا تم تصديرها من هذا الملف واستيرادها في الملف الآخر. إذا كان لدينا عنصر واحد فقط لتصديره في ملف، مثل تعريف فئة (class declaration)، فإننا نستخدم التصدير الافتراضي. بخلاف ذلك، نستخدم التصديرات المسماة. يمكن أيضًا دمج التصديرات الافتراضية والمسماة في ملف واحد لتحقيق أقصى قدر من المرونة.
المعاملات الافتراضية (Default Parameters) في جافاسكريبت
أضاف ES6 ميزة مفيدة جدًا تتمثل في توفير قيم افتراضية للمعاملات عند تعريف الدوال. لنفترض أن لدينا تطبيقًا يعرض رسالة ترحيب للمستخدم بعد تسجيل الدخول:
function showMessage(firstName) {
return "Welcome back, " + firstName;
}
console.log(showMessage('John')); // Welcome back, John
ولكن ماذا لو لم يكن اسم المستخدم متاحًا في قاعدة البيانات لأنه كان حقلاً اختياريًا عند التسجيل؟ في هذه الحالة، قد نرغب في عرض رسالة “Welcome Guest”. قبل ES6، كان علينا التحقق مما إذا كان firstName قد تم توفيره، ثم عرض الرسالة المناسبة:
function showMessage(firstName) {
if (firstName) {
return "Welcome back, " + firstName;
} else {
return "Welcome back, Guest";
}
}
console.log(showMessage('John')); // Welcome back, John
console.log(showMessage()); // Welcome back, Guest
الآن، باستخدام المعاملات الافتراضية للدوال في ES6، يمكننا كتابة الشيفرة أعلاه بشكل أكثر إيجازًا ووضوحًا:
function showMessage(firstName = 'Guest') {
return "Welcome back, " + firstName;
}
console.log(showMessage('John')); // Welcome back, John
console.log(showMessage()); // Welcome back, Guest
يمكننا تعيين أي قيمة كقيمة افتراضية لمعامل الدالة، بما في ذلك القيم المعقدة أو المحسوبة:
function display(a = 10, b = 20, c = b) {
console.log(a, b, c);
}
display(); // 10 20 20
display(40); // 40 20 20
display(1, 70); // 1 70 70
display(1, 30, 70); // 1 30 70
كما ترى، قمنا بتعيين قيم فريدة للمعاملين a وb، ولكن للمعامل c، قمنا بتعيين قيمة b. هذا يعني أن أي قيمة يتم توفيرها لـb سيتم تعيينها أيضًا لـc إذا لم يتم توفير قيمة محددة لـc عند استدعاء الدالة.
في الأمثلة أعلاه، لم نقدم جميع الوسائط للدالة. استدعاءات الدالة ستكون مكافئة لما يلي:
display();هو نفسهdisplay(undefined, undefined, undefined)display(40);هو نفسهdisplay(40, undefined, undefined)display(1, 70);هو نفسهdisplay(1, 70, undefined)
لذا، إذا كانت الوسيطة الممررة هي undefined، فسيتم استخدام القيمة الافتراضية للمعامل المقابل.
يمكننا أيضًا تعيين قيم معقدة أو محسوبة كقيمة افتراضية:
const defaultUser = { name: 'Jane', location: 'NY', job: 'Software Developer' };
const display = (user = defaultUser, age = 60 / 2) => {
console.log(user, age);
};
display();
/* المخرجات
{ name: 'Jane', location: 'NY', job: 'Software Developer' } 30
*/
تبسيط استدعاءات API بالمعاملات الافتراضية
لنلقِ نظرة على مثال عملي يوضح كيف يمكن للمعاملات الافتراضية تبسيط الشيفرة عند إجراء استدعاءات API. في ES5، قد تبدو الشيفرة كما يلي:
// شيفرة ES5
function getUsers(page, results, gender, nationality) {
var params = "";
if (page === 0 || page) { // التحقق من وجود القيمة (بما في ذلك 0)
params += `page=${page}&`;
}
if (results) {
params += `results=${results}&`;
}
if (gender) {
params += `gender=${gender}&`;
}
if (nationality) {
params += `nationality=${nationality}`;
}
fetch('https://randomuser.me/api/?' + params)
.then(function (response) {
return response.json();
})
.then(function (result) {
console.log(result);
})
.catch(function (error) {
console.log('error', error);
});
}
getUsers(0, 10, 'male', 'us');
في هذه الشيفرة، نقوم بإجراء استدعاء API إلى Random User API، ونمرر معلمات اختيارية مختلفة في الدالة getUsers. قبل إجراء استدعاء API، أضفنا العديد من الشروط if للتحقق مما إذا كانت المعلمة قد تم توفيرها أم لا، وبناءً على ذلك نقوم بإنشاء سلسلة الاستعلام (query string) مثل: https://randomuser.me/api/?page=0&results=10&gender=male&nationality=us.
ولكن بدلاً من إضافة كل هذه الشروط if، يمكننا استخدام المعاملات الافتراضية عند تعريف معلمات الدالة، مما يبسط الشيفرة بشكل كبير:
function getUsers(page = 0, results = 10, gender = 'male', nationality = 'us') {
fetch(`https://randomuser.me/api/?page=${page}&results=${results}&gender=${gender}&nationality=${nationality}`)
.then(function (response) {
return response.json();
})
.then(function (result) {
console.log(result);
})
.catch(function (error) {
console.log('error', error);
});
}
getUsers(); // سيتم استخدام القيم الافتراضية
كما ترى، لقد قمنا بتبسيط الشيفرة بشكل كبير. عندما لا نقدم أي وسيطة لدالة getUsers، فإنها ستأخذ القيم الافتراضية. ويمكننا أيضًا توفير قيم خاصة بنا، والتي ستتجاوز القيم الافتراضية:
getUsers(1, 20, 'female', 'gb'); // تجاوز المعاملات الافتراضية
null ليس مساويًا لـundefined في المعاملات الافتراضية
يجب الانتباه إلى نقطة مهمة عند التعامل مع المعاملات الافتراضية: هناك فرق جوهري بين null وundefined. لنلقِ نظرة على الشيفرة التالية:
function display(name = 'David', age = 35, location = 'NY') {
console.log(name, age, location);
}
display('David', 35); // David 35 NY
display('David', 35, undefined); // David 35 NY
في الاستدعاء الأول للدالة display، لم نقدم القيمة الثالثة للمعامل location، وبالتالي ستكون قيمته undefined افتراضيًا، وسيتم استخدام القيمة الافتراضية 'NY'. وينطبق نفس الشيء على الاستدعاء الثاني حيث مررنا undefined صراحة.
ولكن، الاستدعاءات التالية ليست متساوية:
display('David', 35, undefined); // David 35 NY
display('David', 35, null); // David 35 null
عندما نمرر null كوسيطة، فإننا نحدد صراحةً تعيين قيمة null للمتغير location، وهي ليست نفس قيمة undefined. لذلك، لن يتم استخدام القيمة الافتراضية 'NY'، بل ستكون قيمة location هي null.
الدالة Array.prototype.includes في ES7
أضاف معيار ES7 دالة جديدة ومفيدة جدًا تُدعى includes()، والتي تتحقق مما إذا كان عنصر معين موجودًا في المصفوفة أم لا، وتُعيد قيمة منطقية (boolean) إما true أو false. قبل هذه الدالة، كنا نعتمد على طرق أطول للتحقق من وجود عنصر، مثل استخدام indexOf():
// شيفرة ES5
const numbers = ["one", "two", "three", "four"];
console.log(numbers.indexOf("one") > -1); // true
console.log(numbers.indexOf("five") > -1); // false
باستخدام الدالة includes()، يمكن كتابة نفس الشيفرة بشكل أكثر إيجازًا ووضوحًا:
// شيفرة ES7
const numbers = ["one", "two", "three", "four"];
console.log(numbers.includes("one")); // true
console.log(numbers.includes("five")); // false
إن استخدام الدالة includes() يجعل الشيفرة أقصر وأسهل في الفهم والقراءة. كما أنها مفيدة جدًا عند مقارنة قيمة واحدة بعدة قيم محتملة. لنلقِ نظرة على الشيفرة التالية:
const day = "monday";
if (day === "monday" || day === "tuesday" || day === "wednesday") {
// قم بتنفيذ بعض الإجراءات
}
يمكن تبسيط الشيفرة أعلاه باستخدام الدالة includes() على النحو التالي:
const day = "monday";
if (["monday", "tuesday", "wednesday"].includes(day)) {
// قم بتنفيذ بعض الإجراءات
}
لذلك، تُعد الدالة includes() أداة عملية للغاية عند التحقق من وجود قيم داخل مصفوفة.
نقاط ختامية
لقد شهدت جافاسكريبت منذ إصدار ES6 العديد من التغييرات والتحسينات الجوهرية التي أثرت بشكل كبير على طريقة كتابة الشيفرة وتطوير التطبيقات. يجب على كل مطور جافاسكريبت، سواء كان يعمل مع مكتبات مثل React أو أطر عمل مثل Angular وVue، أن يكون على دراية تامة بهذه الميزات الحديثة.
إن إتقان هذه المفاهيم لا يجعلك مطورًا أفضل فحسب، بل يعزز أيضًا فرصك المهنية في سوق العمل التنافسي. إذا كنت في بداية رحلتك لتعلم هذه المكتبات والأطر، فإن فهم هذه الميزات الجديدة سيمنحك أساسًا متينًا للانطلاق.
لتعميق فهمك لميزات جافاسكريبت الحديثة، يُنصح دائمًا بالاطلاع على الموارد التعليمية الموثوقة والكتب المتخصصة التي تغطي هذه التحديثات بشكل شامل. هناك العديد من المصادر المتاحة التي يمكن أن تكون دليلك الوحيد لإتقان مفاهيم جافاسكريبت الحديثة.

نحن نشجعك على مواصلة التعلم والاستكشاف في عالم جافاسكريبت المتجدد باستمرار. إن المعرفة المستمرة هي مفتاح التميز في هذا المجال.
الخلاصة التقنية
يمثل التطور المستمر للغة جافاسكريبت، بدءًا من ES6 وما بعدها، نقلة نوعية في تطوير الويب. لقد أدت إضافة ميزات مثل let وconst إلى تعزيز إدارة نطاق المتغيرات وتقليل الأخطاء الشائعة، بينما قدمت الوعود (Promises) حلًا أنيقًا وفعالًا للتعامل مع العمليات غير المتزامنة، مما يسهل بناء تطبيقات أكثر استجابة. كما أحدث نظام الوحدات (modules) عبر import وexport ثورة في تنظيم الشيفرة وإدارتها، مما سمح ببناء تطبيقات معيارية وقابلة للصيانة. أخيرًا، ساهمت المعاملات الافتراضية (Default Parameters) ودالة Array.prototype.includes في تبسيط الشيفرة وتحسين قابليتها للقراءة. هذه الميزات مجتمعة لا غنى عنها لأي مطور جافاسكريبت حديث يسعى لكتابة شيفرة نظيفة، فعالة، ومواكبة لأحدث المعايير.