JavaScript الحديثة: شرح عملي لـ import وexport وlet وconst وPromises في ES6+

دقائق القراءة: 12

شهدت لغة JavaScript خلال السنوات الأخيرة تطورات كبيرة غيّرت طريقة كتابة التطبيقات الحديثة. ولم تعد معرفة الأساسيات التقليدية كافية للمطور الذي يريد كتابة شيفرة أوضح، أكثر أماناً، وأسهل في الصيانة. لذلك أصبحت ميزات ES6+ جزءاً أساسياً من العمل اليومي، سواء كنت تبني واجهات باستخدام React أو تعمل على مشاريع تعتمد على Angular أو Vue.

شرح JavaScript الحديثة وأهم ميزات ES6 مثل let و const و promises و import export

في هذا الدليل سنستعرض مجموعة من أهم المفاهيم التي يجب أن يعرفها كل مطور JavaScript حديث، مع أمثلة عملية وشرح يركز على الفهم الحقيقي لا مجرد حفظ الصياغة.

لماذا تُعد ميزات ES6+ مهمة لكل مطور JavaScript؟

الانتقال إلى أساليب JavaScript الحديثة لا يمنحك شيفرة أجمل فقط، بل يقدم فوائد عملية واضحة:

  • تقليل الأخطاء الناتجة عن إعادة تعريف المتغيرات بشكل غير مقصود.
  • تحسين تنظيم الملفات عبر نظام الوحدات باستخدام modules.
  • تبسيط التعامل مع العمليات غير المتزامنة بواسطة Promises.
  • جعل الشيفرة أكثر وضوحاً وقرباً من المنطق الذي تريد تنفيذه.
  • رفع جاهزيتك للعمل على مكتبات وأطر حديثة.

فهم let وconst في JavaScript

قبل ظهور ES6 كان المطورون يعتمدون غالباً على var، وهي كلمة مفتاحية تعتمد على نطاق الدالة function scope أو النطاق العام global scope. المشكلة أن هذا السلوك كان يسبب التباساً في كثير من الحالات، خصوصاً داخل الشروط والحلقات.

مع ظهور let وconst أصبح لدينا ما يسمى block scope، أي أن المتغير يصبح مرتبطاً بالكتلة المحصورة بين الأقواس المعقوفة.

كيف يعمل let؟

عند تعريف متغير باستخدام let يمكنك تغيير قيمته لاحقاً، لكن لا يمكنك إعادة تعريفه بالاسم نفسه داخل النطاق نفسه.

// ES5 Code
var value = 10;
console.log(value); // 10

var value = "hello";
console.log(value); // hello

var value = 30;
console.log(value); // 30

في المثال السابق سمح var بإعادة تعريف المتغير value عدة مرات، وهذا قد يسبب أخطاء يصعب تتبعها.

// ES6 Code
let value = 10;
console.log(value); // 10

let value = "hello"; // Uncaught SyntaxError: Identifier 'value' has already been declared

أما إعادة الإسناد فهي مسموحة مع let لأنها لا تعني إعادة تعريف المتغير:

// ES6 Code
let value = 10;
console.log(value); // 10

value = "hello";
console.log(value); // hello

نطاق الكتلة مع if وfor

يوضح المثال التالي الفرق بين var وlet داخل كتلة شرطية:

// ES5 Code
var isValid = true;
if (isValid) {
  var number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10
// ES6 Code
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.

وإذا وُجد متغير آخر بالاسم نفسه خارج الكتلة، فسيبقى كل متغير في نطاقه الخاص:

// ES6 Code
let isValid = true;
let number = 20;

if (isValid) {
  let number = 10;
  console.log('inside:', number); // inside: 10
}

console.log('outside:', number); // outside: 20

وينطبق الأمر نفسه على الحلقات:

// ES5 Code
for (var i = 0; i < 10; i++) {
  console.log(i);
}
console.log('outside:', i); // 10
// ES6 Code
for (let i = 0; i < 10; i++) {
  console.log(i);
}
console.log('outside:', i); // Uncaught ReferenceError: i is not defined

هذا السلوك يجعل 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

لاحظ أن المتغير الداخلي لا يؤثر على قيمة المتغير الخارجي، لأن لكل واحد منهما نطاقه المستقل.

متى تستخدم const بدلاً من let؟

تعمل const مثل let من حيث block scope، لكن الاختلاف الجوهري هو أن إعادة الإسناد غير مسموحة.

let number = 10;
number = 20;
console.log(number); // 20
const number = 10;
number = 20; // Uncaught TypeError: Assignment to constant variable.

كما لا يمكن إعادة تعريف المتغير نفسه:

const number = 20;
console.log(number); // 20

const number = 10; // Uncaught SyntaxError: Identifier 'number' has already been declared

لماذا يمكن تعديل المصفوفات والكائنات رغم استخدام const؟

هذا سؤال شائع جداً. عند استخدام const مع المصفوفات أو الكائنات، فإن الثابت هو المرجع reference لا المحتوى الداخلي نفسه.

const arr = [1, 2, 3, 4];
arr.push(5);
console.log(arr); // [1, 2, 3, 4, 5]

في هذا المثال لم يتغير المرجع المخزن في arr، بل تغيرت البيانات الموجودة في نفس المكان.

const obj = { name: 'David', age: 30 };
obj.age = 40;
console.log(obj); // { name: 'David', age: 40 }

لكن تغيير المرجع نفسه غير مسموح:

const obj = { name: 'David', age: 30 };
const obj1 = { name: 'Mike', age: 40 };
obj = obj1; // Uncaught TypeError: Assignment to constant variable.

وينطبق الأمر ذاته على المصفوفات:

const arr = [1, 2, 3, 4];
arr = [10, 20, 30]; // Uncaught TypeError: Assignment to constant variable.

ملخص عملي لاستخدام let وconst

  • استخدم const افتراضياً عندما لا تحتاج إلى إعادة إسناد المتغير.
  • استخدم let عندما تتوقع تغير القيمة لاحقاً.
  • تجنب var في المشاريع الحديثة إلا عند الحاجة إلى دعم قديم جداً أو مراجعة كود تراثي.

ما هي Promises في JavaScript؟

تُستخدم Promises للتعامل مع العمليات غير المتزامنة asynchronous operations، مثل جلب البيانات من API أو تنفيذ مهمة تستغرق وقتاً قبل إرجاع النتيجة.

بدلاً من التوقف الفعلي حتى تكتمل العملية، تسمح لك Promise بمتابعة تنفيذ البرنامج ثم التعامل مع النتيجة لاحقاً عند نجاح العملية أو فشلها.

حالات Promise الثلاث

  • Pending: العملية ما زالت قيد التنفيذ.
  • Fulfilled: تم التنفيذ بنجاح.
  • Rejected: فشلت العملية.

كيف تنشئ Promise؟

const promise = new Promise(function (resolve, reject) {
});

يستقبل المُنشئ Promise دالة تحتوي على معاملين: resolve وreject. الأول يُستخدم عند النجاح، والثاني عند الفشل.

ولمحاكاة عملية تستغرق وقتاً سنستخدم setTimeout():

const promise = new Promise(function (resolve, reject) {
  setTimeout(function () {
    const sum = 4 + 5;
    resolve(sum);
  }, 2000);
});

استخدام .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() تنتقل القيمة إلى المعالج .then().

التعامل مع الفشل عبر .catch()

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);
});

خطأ Promise بدون استخدام catch في JavaScript

في هذه الحالة سيظهر خطأ غير مُعالج لأننا استخدمنا reject() من دون معالج للأخطاء.

promise
  .then(function (result) {
    console.log(result);
  })
  .catch(function (error) {
    console.log(error);
  });

معالجة أخطاء Promise باستخدام catch في JavaScript

إضافة .catch() ممارسة مهمة تمنع توقف التطبيق بشكل مفاجئ وتسهل تتبع الأخطاء.

سلسلة الوعود Promise Chaining

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() تنتقل تلقائياً إلى المعالج التالي، وهذا ما يُعرف باسم promise chaining.

مثال على Promise chaining في JavaScript

تأخير إنشاء Promise عبر دالة

في بعض الحالات لا تريد إنشاء Promise فوراً، بل بعد تمرير قيم معينة. الحل هو إرجاعها من داخل دالة:

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
});

إنشاء Promise ديناميكية عبر دالة في JavaScript

تمرير أكثر من قيمة في 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);
  })
  .catch(function (error) {
    console.log(error);
  });

تمرير كائن داخل resolve في Promise بلغة JavaScript

استخدام Arrow Functions مع Promises

يمكن كتابة المثال السابق بصياغة أحدث وأكثر اختصاراً:

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);
});

شرح import وexport في ES6 Modules

قبل ES6 كان استيراد ملفات JavaScript يتم غالباً عبر عدة وسوم 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>

هذا الأسلوب قد يسبب تضارب أسماء بين الملفات. أما نظام modules فيجعل كل ملف وحدة مستقلة، ولا تصبح المتغيرات أو الدوال متاحة لملفات أخرى إلا إذا تم تصديرها صراحة.

أنواع التصدير في JavaScript

  • Named Exports: يمكن أن يوجد أكثر من تصدير مسمى في الملف الواحد.
  • Default Export: يمكن أن يوجد تصدير افتراضي واحد فقط في كل ملف.

التصدير المسمى Named Export

export const temp = "This is some dummy text";

وإذا كان لديك أكثر من عنصر للتصدير:

const temp1 = "This is some dummy text1";
const temp2 = "This is some dummy text2";

export { temp1, temp2 };

أما الصياغة التالية فهي غير صحيحة:

// Invalid syntax
export { key1: value1, key2: value2 };

استيراد Named Exports

import { temp1, temp2 } from './filename';

ولا تحتاج عادة إلى كتابة امتداد .js عند الاستيراد:

import { temp1, temp2 } from './functions';
import { temp1 } from '../functions';

ومن المهم أن يطابق الاسم المستورد الاسم المصدَّر:

// constants.js
export const PI = 3.14159;
import { PI } from './constants';

أما هذا فلن يعمل:

import { PiValue } from './constants';

إعادة التسمية أثناء الاستيراد أو التصدير

إذا احتجت إلى اسم مختلف، استخدم as:

import { PI as PIValue } from './constants';

كما يمكن إعادة التسمية عند التصدير:

// constants.js
const PI = 3.14159;
export { PI as PIValue };
import { PIValue } from './constants';

مثال عملي على Named Exports

// 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";
  }
}

export { isValidEmail, isValidPhone, isEmpty };
// index.js
import { isEmpty, isValidEmail } from "./utils/validations";

console.log("isEmpty:", isEmpty("abcd"));
console.log("isValidEmail:", isValidEmail("abc@11gmail.com"));
console.log("isValidEmail:", isValidEmail("ab@c@11gmail.com"));

ميزة هذا الأسلوب أنك تستورد فقط ما تحتاجه، وبأي ترتيب.

التصدير الافتراضي 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;
import NAME, { PI, AGE } from './constants';

ميزة مهمة في Default Export

يمكنك اختيار أي اسم عند الاستيراد:

// constants.js
const AGE = 30;
export default AGE;
import myAge from './constants';
console.log(myAge); // 30

وذلك لأن الملف لا يحتوي إلا على تصدير افتراضي واحد.

استيراد كل شيء عبر import *

import * as constants from './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

دمج التصدير المسمى والافتراضي في سطر واحد

// 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 };
import USER, { PI, AGE, USERNAME } from "./constants";

المعاملات الافتراضية Default Parameters

أضافت ES6 طريقة سهلة جداً لتحديد قيم افتراضية لمعلمات الدوال.

function showMessage(firstName) {
  return "Welcome back, " + firstName;
}

console.log(showMessage('John'));

إذا كانت القيمة غير موجودة، فقبل ES6 كنا نضطر لاستخدام شروط:

function showMessage(firstName) {
  if (firstName) {
    return "Welcome back, " + firstName;
  } else {
    return "Welcome back, Guest";
  }
}

أما الآن فيمكن كتابة ذلك مباشرة:

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

هنا يحصل c على قيمة b إذا لم تُمرر له قيمة صريحة.

استخدام قيم مركبة أو محسوبة

const defaultUser = {
  name: 'Jane',
  location: 'NY',
  job: 'Software Developer'
};

const display = (user = defaultUser, age = 60 / 2) => {
  console.log(user, age);
};

display();

تبسيط بناء API Query String

من التطبيقات الممتازة للمعاملات الافتراضية تقليل عدد الشروط عند بناء روابط الطلبات:

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(1, 20, 'female', 'gb');

بهذا الأسلوب تصبح الدالة أوضح وأسهل في القراءة والتوسعة.

الفرق بين null وundefined

عند استخدام القيم الافتراضية يجب الانتباه إلى أن undefined يفعّل القيمة الافتراضية، بينما null لا يفعل ذلك.

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('David', 35, null); // David 35 null

التمرير بـ null يعني أنك تريد تعيين قيمة فارغة عمداً، وليس استخدام القيمة الافتراضية.

استخدام Array.prototype.includes() في ES7

أضافت ES7 الدالة includes() للتحقق من وجود عنصر داخل المصفوفة وإرجاع قيمة منطقية true أو false.

// ES5 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.indexOf("one") > -1); // true
console.log(numbers.indexOf("five") > -1); // false
// ES7 Code
const numbers = ["one", "two", "three", "four"];
console.log(numbers.includes("one")); // true
console.log(numbers.includes("five")); // false

الصياغة الجديدة أوضح وأقصر، خصوصاً في حالات المقارنة المتعددة:

const day = "monday";

if (["monday", "tuesday", "wednesday"].includes(day)) {
  // do something
}

أفضل الممارسات عند كتابة JavaScript الحديثة

  • اجعل const خيارك الأول، وانتقل إلى let فقط عند الحاجة.
  • أضف .catch() إلى كل Promise تقريباً لتفادي الأخطاء غير المعالجة.
  • استخدم modules لتنظيم الشيفرة بدلاً من تجميع كل شيء في ملف واحد.
  • اعتمد المعاملات الافتراضية لتقليل الشروط المتكررة.
  • اختر الأساليب الأوضح، مثل includes() بدلاً من المقارنات المعقدة.

لماذا تعزز هذه المفاهيم جودة الكود؟

القيمة الحقيقية لهذه الميزات لا تكمن في كونها أحدث فقط، بل في أنها تقلل الغموض وتزيد قابلية الفهم. فعندما يقرأ مطور آخر شيفرتك ويلاحظ استخدام let وconst بشكل صحيح، وسلاسل Promises منظمة، ووحدات import/export مقسمة بوضوح، يصبح المشروع أسهل في الصيانة، وأسرع في التطوير، وأقل عرضة للأخطاء.

غلاف كتاب عن JavaScript الحديثة وميزات ES6 المتقدمة

الخلاصة التقنية

إذا أردت كتابة شيفرة JavaScript حديثة واحترافية، فابدأ بإتقان let وconst لفهم النطاق وإدارة المتغيرات، ثم تعلم Promises للتعامل السليم مع العمليات غير المتزامنة، وبعدها نظّم مشروعك باستخدام import وexport. هذه المفاهيم ليست إضافات تجميلية، بل أساس عملي لبناء تطبيقات أكثر استقراراً ووضوحاً وقابلية للتوسع.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *