جافاسكربت الحديثة: شرح عملي لـ import وexport وlet وconst وPromises في ES6+

دقائق القراءة: 12
شرح ميزات جافاسكربت الحديثة مثل الاستيراد والتصدير والمتغيرات والوعود البرمجية في ES6

مقدمة إلى جافاسكربت الحديثة

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

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

في هذا الدليل سنستعرض أهم المفاهيم التي ينبغي لكل مطور جافاسكربت معرفتها، مثل let وconst وPromises وimport/export وdefault parameters وincludes().

الفرق بين let وconst وvar في جافاسكربت

قبل ظهور ES6 كان الاعتماد الأكبر على الكلمة المفتاحية var، والتي تعمل ضمن نطاق الدالة function scope أو النطاق العام global scope. المشكلة أنها لا تدعم نطاق الكتلة block scope بالشكل الذي يحتاجه المطورون في التطبيقات الحديثة.

مع ظهور let وconst أصبح بالإمكان التحكم بالنطاق البرمجي على مستوى الكتل مثل جمل if والحلقات for والأقواس المعقوفة { }.

كيف تعمل 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

نطاق الكتلة مع let

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

// ES5 Code
var isValid = true;
if (isValid) {
  var number = 10;
  console.log('inside:', number); // inside: 10
}
console.log('outside:', number); // outside: 10

المتغير المعرّف بواسطة var يظل متاحاً خارج كتلة if.

// 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

هنا يظهر أثر block scope بوضوح: المتغير number متاح فقط داخل الكتلة التي عُرّف فيها.

وإذا كان هناك متغير آخر بالاسم نفسه خارج الكتلة، فلن يحدث تعارض:

// 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

استخدام let داخل الحلقات

// 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

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

متى نستخدم const؟

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

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

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

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]
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

  • let مناسب للمتغيرات التي قد تتغير قيمتها لاحقاً.
  • const مناسب للقيم التي لا يجب إعادة إسنادها.
  • كلاهما يدعم block scope بعكس var.
  • استخدامهما يقلل من الأخطاء غير المتوقعة في الشيفرة.

فهم Promises في جافاسكربت

تُعد Promises من أهم مفاهيم جافاسكربت الحديثة، خصوصاً عند التعامل مع العمليات غير المتزامنة asynchronous operations مثل جلب البيانات من API أو قراءة الملفات أو انتظار نتائج عمليات تستغرق وقتاً.

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

حالات Promise

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

إنشاء Promise

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

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

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

في المثال السابق سيتم حل الوعد بعد ثانيتين بالقيمة 9.

التعامل مع النجاح عبر .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
});

التعامل مع الأخطاء عبر .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 في جافاسكربت

عند رفض الوعد دون معالجة الخطأ ستظهر رسالة خطأ غير ملتقطة. لذلك من الأفضل دائماً إضافة .catch().

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

معالجة أخطاء Promise باستخدام 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.

شرح تسلسل then في Promise chaining داخل جافاسكربت

تأخير إنشاء 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 في جافاسكربت

تمرير أكثر من قيمة عند 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 في جافاسكربت

استخدام 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);
});

استخدام arrow functions يجعل الشيفرة أقصر وأكثر انتشاراً في المشاريع الحديثة.

شرح import وexport في ES6 Modules

قبل نظام الوحدات modules في ES6 كان المطورون يعتمدون على عدة وسوم 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>

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

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

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

ثم تستوردها هكذا:

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

يجب أن يتطابق الاسم عند الاستيراد مع الاسم عند التصدير:

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

وإذا احتجت إلى اسم آخر، استخدم إعادة التسمية:

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

بما أن الملف يملك تصديراً افتراضياً واحداً فقط، يمكنك تسميته كما تشاء عند الاستيراد.

تصدير كائن مباشرة

// 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
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();
display(40);
display(1, 70);
display(1, 30, 70);

إذا لم تُمرّر قيمة، فسيتم استخدام undefined ضمنياً، وعندها تؤخذ القيمة الافتراضية.

قيم افتراضية معقدة أو محسوبة

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

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

display();

تبسيط استدعاءات API بالمعاملات الافتراضية

function getUsers(page, results, gender, nationality) {
  var params = "";

  if (page === 0 || page) {
    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);
    });
}

يمكن كتابة الشيفرة بصورة أوضح باستخدام default parameters:

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

استخدام 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 (day === "monday" || day === "tuesday" || day === "wednesday") {
  // do something
}

يمكن اختصارها إلى:

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

لماذا يجب أن تتقن ميزات ES6+؟

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

  • let وconst يحدّان من أخطاء النطاق وإعادة التعريف.
  • Promises تنظّم العمليات غير المتزامنة بشكل أفضل.
  • import وexport يقدمان بنية نظيفة وقابلة للتوسع.
  • default parameters تقلل من الشيفرة المتكررة.
  • includes() تجعل التحقق من القيم أوضح وأسرع فهماً.

غلاف كتاب عن تعلم جافاسكربت الحديثة وميزات ES6 للمطورين

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

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

اترك تعليقاً

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