دليل شامل للغة جافا سكريبت: الأساسيات والمواضيع المتقدمة

Amine
05/01/2025

تُعَدّ لغة JavaScript (جافا سكريبت) واحدة من أهم لغات البرمجة في تطوير الويب الحديث. فهي لغة برمجة عالية المستوى ديناميكية وذات نظام أنواع مرن، تُنفَّذ بشكل رئيسي في متصفحات الويب لإضفاء الحيوية والتفاعلية على صفحات HTML. منذ إنشائها في منتصف التسعينات من قبل Brendan Eich، تطورت جافا سكريبت بشكل كبير وأصبحت لغة متعددة الاستعمالات؛ حيث يُمكن تشغيلها أيضًا خارج المتصفح (مثال: عبر منصة Node.js على جهة الخادم). في هذا الدليل الشامل، سنستعرض أساسيات لغة جافا سكريبت بالتفصيل، ونغطّي بعض المواضيع المتقدمة لفهم أعمق لهذه اللغة وكيفية استخدامها بشكل فعال في تطوير تطبيقات الويب.

المتغيرات (Variables)

المتغيرات هي حاويات لتخزين البيانات في البرامج. في جافا سكريبت، نقوم بتعريف المتغيرات باستخدام كلمات مفتاحية خاصة هي var وlet وconst. يحدد نوع الكلمة المفتاحية خصائص المتغير والنطاق الذي يعيش فيه وإمكانية تعديل قيمته. فيما يلي ملخّص للفرق بين هذه الكلمات المفتاحية:

  • var: يُعرّف المتغير بنطاق دالة (Function Scope)، أي يكون متاحًا في أي مكان ضمن الدالة المُعرَّف داخلها. يمكن أيضًا إعادة التصريح عن نفس المتغير باستخدام var في النطاق نفسه. يتميز var بأن تعريفه يُرفع تلقائيًا إلى أعلى نطاقه عند تنفيذ البرنامج (Hoisting).
  • let: يُعرّف المتغير بنطاق كتلة (Block Scope)، أي أنه محصور ضمن الأقواس أو البلوك الذي تم تعريفه بداخله فقط. لا يمكن إعادة تعريف نفس المتغير باستخدام let في نفس النطاق. بعكس var, المتغير المُعرَّف باستخدام let لا يتم رفعه إلى أعلى النطاق؛ محاولة استخدامه قبل تعريفه تؤدي إلى خطأ.
  • const: مشابه لـlet من حيث النطاق (نطاق كتلة)، ولكن مع فرق أساسي: يجب إسناد قيمة ابتدائية عند تعريف المتغير، ولا يمكن إعادة تعيين (تغيير) تلك القيمة لاحقًا طوال فترة حياة البرنامج. أيضًا، لا يمكن إعادة تعريف نفس المتغير المُعرَّف بـconst في نفس النطاق.
  • جدير بالذكر أنه في حالة كان المتغير المُعرَّف باستخدام const كائنًا (object) أو مصفوفة، يمكن تعديل خصائص ذلك الكائن أو عناصر المصفوفة بالرغم من عدم إمكانية تغيير مرجع المتغير نفسه. أي أن الثابت يمنع تغيير المرجع الأساسي، لكنه لا يجعل الكائن نفسه ثابتًا بالكامل.

يُفضل استخدام let وconst في معظم الحالات الحديثة بدلاً من var، وذلك لتفادي المشاكل المتعلقة بالنطاق والرفع وصعوبة تعقب الأخطاء. عادةً ما يُستخدم const للمتغيرات التي لن تتغير قيمتها، بينما يُستخدم let للمتغيرات القابلة للتغيير.

// مثال يوضح الفروقات بين var و let و const:

// سيُطبع undefined هنا بسبب رفع تعريف متغير var للأعلى
console.log(x); // undefined
var x = 5;
var x = 10; // إعادة تعريف المتغير var
console.log(x); // 10

// محاولة استخدام متغير قبل تعريفه باستخدام let ستؤدي إلى خطأ
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 15;
// let y = 20; // SyntaxError: Identifier 'y' has already been declared
y = 20; // تغيير قيمة المتغير المعرّف بـ let مسموح
console.log(y); // 20

const z = 30;
// z = 40; // TypeError: Assignment to constant variable.
console.log(z); // 30

const obj = { name: "Ali" };
obj.name = "Omar"; // مسموح تعديل خصائص الكائن حتى لو عرّف بـ const
console.log(obj.name); // "Omar"

أنواع البيانات (Data Types)

تدعم جافا سكريبت عدة أنواع مختلفة من البيانات. يمكن للمتغير الواحد أن يحمل أي نوع من هذه الأنواع في أوقات مختلفة نظرًا لكون اللغة ضعيفة التقييد في الأنواع (dynamic typing). بشكل عام، تنقسم الأنواع إلى فئتين: الأنواع البدائية (Primitive Types) والأنواع المرجعية (Reference Types). فيما يلي أبرز أنواع البيانات في جافا سكريبت:

  • رقمي (Number): يمثل القيم العددية. تستخدم جافا سكريبت نوعًا رقميًا واحدًا لجميع الأرقام الصحيحة والعشرية على حد سواء (تعتمد على النظام العشري ذو الفاصلة العائمة بدقة مزدوجة 64-بت). تتضمن القيم العددية أيضًا قيمًا خاصة مثل NaN (Not a Number) الناتج عن عمليات غير صالحة على الأرقام، وInfinity و-Infinity الناتجة عن تجاوز حدود الأرقام.
  • نصي (String): يمثل سلاسل الأحرف والنصوص. يمكن كتابة النصوص بين علامات تنصيص مفردة أو مزدوجة '...'/"...". منذ ES6، يمكن أيضًا استخدام العلامات العكسية (`) لتعريف نصوص متعددة الأسطر أو تتضمن تعابير مدمجة (تسمى template literals) باستخدام صيغة ${...}.
  • منطقي (Boolean): يمثل قيمة منطقية صحيحة أو خاطئة (true أو false). تستخدم القيم المنطقية عادةً في عمليات المقارنة والتحقق من الشروط.
  • فارغ (null): نوع خاص يشير إلى غياب قيمة (قيمة فارغة). يمكن تعيينه للمتغير للإشارة إلى أنه “لا يوجد قيمة” بشكل مقصود. (ملاحظة: التعبير typeof null يعيد “object” بسبب خلل قديم في اللغة، على الرغم من أن null ليس كائنًا).
  • غير معرّف (undefined): قيمة تلقائية يحصل عليها أي متغير لم يتم إسناد قيمة له بعد، أو أي دالة لا تعيد قيمة صراحةً. تشير إلى “غياب قيمة” بشكل غير مقصود أو افتراضي.
  • رمز (Symbol): نوع بدائي جديد تم تقديمه في ES6، يُستخدم لإنشاء معرفات فريدة. غالبًا ما تُستخدم الـSymbols كمفاتيح للخصائص في الكائنات لتجنب التعارضات بين أسماء الخصائص، ولتمييز القيم بشكل فريد.
  • BigInt: نوع بدائي تم تقديمه في ES2020 لتمثيل الأعداد الصحيحة الكبيرة التي تتجاوز نطاق Number العادي (أكبر من 253). يمكن إنشاء قيمة BigInt بإضافة حرف n إلى نهاية الرقم مثل 123n. يسمح BigInt بالتعامل الدقيق مع الأعداد الصحيحة الكبيرة دون فقدان الدقة.
  • كائن (Object): نوع مرجعي يُستخدم لتمثيل كائنات معقدة تحتوي على مجموعة من القيم (خصائص properties) ووظائف (أساليب methods). يشمل هذا النوع كل من الكائنات المعرّفة من قبل المستخدم وهياكل بيانات مثل المصفوفات (Array) والدوال (Function) وغيرها. يتم تخزين الكائنات بالمرجع (reference) وليس بالقيمة.

الأنواع الستة الأولى المذكورة أعلاه (Number، String، Boolean، null، undefined، Symbol) بالإضافة إلى BigInt تُسمى “أنواع بدائية” لأنها تمثل قيمة مفردة غير قابلة للتغيير (immutable). في المقابل، يعتبر نوع Object غير بدائي، والقيم الكائنية تُمرَّر وتُخزَّن كمراجع. هذا يعني أنه عند نسخ كائن أو تمريره لدالة فإن المرجع هو الذي يُنقل وليس نسخة منفصلة من القيمة. من المهم فهم هذا الاختلاف بين القيم البدائية والكائنات أثناء التعامل مع المتغيرات والمعاملات.

بإمكانك استخدام المعامل typeof للحصول على نوع القيمة أثناء التنفيذ. كما تقوم جافا سكريبت أحيانًا بتحويل نوع القيمة تلقائيًا وفق السياق (يطلق على ذلك Type Coercion). على سبيل المثال، عند جمع قيمة نصية مع قيمة رقمية باستخدام المعامل +، يتم تحويل الرقم إلى نص ثم دمجهما معًا كنص واحد. سنستعرض المزيد حول تحويل الأنواع الضمني عند مناقشة العوامل (Operators) أدناه.

// مثال يوضح الطبيعة الديناميكية للأنواع في جافا سكريبت واستخدام typeof:
let value;
console.log(typeof value); // "undefined"
value = 42;
console.log(typeof value); // "number"
value = "مرحبا";
console.log(typeof value); // "string"
value = true;
console.log(typeof value); // "boolean"
value = 123n;
console.log(typeof value); // "bigint"
value = Symbol("demo");
console.log(typeof value); // "symbol"
value = null;
console.log(typeof value); // "object" (نوع null يعتبر كائنًا بسبب خلل قديم)

العوامل (Operators)

توفّر جافا سكريبت مجموعة غنية من العوامل لإجراء العمليات على القيم. تشمل هذه العوامل العمليات الحسابية، والمقارنات المنطقية، وإسناد القيم، وغير ذلك. على سبيل المثال، العوامل الحسابية الأساسية هي + (الجمع أو دمج النصوص)، - (الطرح), * (الضرب), / (القسمة), % (باقي القسمة), و** (الأس). عامل + قد يُستخدم لكل من جمع الأرقام ودمج السلاسل النصية كما ذكرنا سابقًا. هناك أيضًا عوامل إسناد مثل = و+= و-= وغيرها لتحديث قيم المتغيرات.

عوامل المقارنة تُستخدَم للمقارنة بين القيم، وتُعيد قيمة منطقية (true أو false). تشمل هذه العوامل > و< و>= و<= للمقارنات الرياضية، بالإضافة إلى == و=== (وعدم المساواة !=/!==). الفرق الجوهري بين عاملي المساواة == و=== هو أن الأول يقوم بتحويل الأنواع ضمنيًا (Loose Equality) قبل المقارنة، أما الثاني فيتحقق من المساواة الصارمة دون أي تحويل (Strict Equality). بشكل عام، يُنصح باستخدام المساواة الصارمة === لتجنب السلوك غير المتوقع الناجم عن تحويل الأنواع التلقائي.

// مقارنة المساواة: الفرق بين == و ===
console.log(5 == "5");   // true  لأن "5" تُحوّل إلى الرقم 5 قبل المقارنة
console.log(5 === "5");  // false لأن النوع مختلف (رقم مقابل نص)
console.log(0 == false); // true  لأن false يُحوّل إلى 0
console.log(0 === false);// false لأن النوع مختلف (رقم مقابل منطقي)
console.log(null == undefined);  // true  كلاهما يُعتبر "لا قيمة"
console.log(null === undefined); // false الأنواع مختلفة تمامًا

أما العوامل المنطقية (&& للـAND المنطقي، و|| للـOR المنطقي، و! للنفي المنطقي) فتُستخدم للتحقق من أكثر من شرط أو عكس قيمة منطقية. في جافا سكريبت، يمكن استعمال القيم غير المنطقية في تعابير منطقية، حيث تُعتبر بعض القيم truthy (تعامل كأنها true) والبعض الآخر falsy (تعامل كأنها false). على سبيل المثال، القيم 0، "" (سلسلة فارغة)، null, undefined, وNaN كلها تُعتبر falsy، أما بقية القيم فتعتبر truthy. نتيجة لذلك، || و&& يمكن استخدامهما لتقديم قيم افتراضية: التعبير expr1 || expr2 يعيد expr1 إذا كان truthy وإلا يعيد expr2، بينما expr1 && expr2 يعيد expr2 فقط إذا كان كل من expr1 وexpr2 truthy (وإلا يعيد القيمة falsy الأولى).

توفر اللغة أيضًا عوامل أحدث للتعامل مع الحالات الخاصة بالقيم null أو undefined. من ذلك عامل الدمج الصفري ?? الذي يعيد القيمة اليمنى فقط إذا كانت اليسرى null أو undefined (على عكس || الذي يعيد اليمنى إذا كانت اليسرى أي قيمة falsy حتى لو كانت 0 أو سلسلة فارغة). مثال: let name = null; console.log(name ?? "غير معروف"); الناتج سيكون “غير معروف” لأن name null. أما عامل السلسلة الاختيارية ?. فيسمح بالوصول الآمن لسلاسل من الخصائص قد تكون غير معرّفة، مثلاً: obj.val?.prop تعيد undefined بدلًا من خطأ إذا كان obj.val null أو undefined. هذه العوامل الجديدة تساعد في كتابة كود أكثر أمانًا واختصارًا عند التعامل مع قيم قد تكون غائبة.

أخيرًا، هناك عامل الشرط الثلاثي (Conditional/Ternary Operator) المكتوب على شكل condition ? expr1 : expr2. يقوم هذا العامل بتقييم الشرط المنطقي؛ فإن كان صحيحًا يعيد expr1 وإلا يعيد expr2. يُستخدم العامل الثلاثي كاختصار لكتابة جملة if...else بسيطة ضمن تعبير واحد. مثال: let age = 18; let result = (age >= 18) ? "بالغ" : "قاصر";.

التحكم في التدفق (Control Flow)

تسمح بُنى التحكم في التدفق بتوجيه مسار تنفيذ البرنامج بناءً على شروط معينة أو تكرار أجزاء من الشيفرة. في جافا سكريبت، لدينا بيانات شرطية (مثل if وswitch) وتراكيب للحلقات (مثل for وwhile) للتحكم بتنفيذ الكود.

الجمل الشرطية: تستخدم جملة if للتحقق من شرط وتنفيذ كتلة من الكود إذا تحقق الشرط. يمكن إضافة else لتنفيذ كتلة بديلة إذا كان الشرط خاطئًا، وelse if للتحقق من شرط آخر في حالة عدم تحقق الشرط الأول. على سبيل المثال، يمكننا التحقق من إشارة رقم وإظهار رسالة مختلفة لكل حالة (موجب، سالب، أو صفر). هناك أيضًا جملة switch التي تسمح بفحص قيمة معينة مقابل عدة حالات محتملة (cases) وتنفيذ الفرع المطابق؛ تُستخدم عادةً عند وجود العديد من الحالات المحتملة المرتبطة بقيمة واحدة.

الحلقات التكرارية: تمكّننا من تكرار تنفيذ كتلة من التعليمات عدة مرات. حلقة for هي الأكثر استخدامًا للتكرار مع عدّاد، حيث تتيح تحديد قيمة ابتدائية للعداد، وشرط استمرار الحلقة، والتحديث بعد كل دورة. حلقات while وdo...while توفر أسلوبًا للتكرار بناءً على شرط يستمر إلى أن يصبح خاطئًا. في حلقة do...while يتم تنفيذ الجسم مرة واحدة على الأقل قبل التحقق من الشرط. يمكن استخدام الكلمتين المفتاحيتين break لإنهاء الحلقة قبل اكتمالها، وcontinue لتخطي الدورة الحالية ومتابعة التكرار التالي.

بالإضافة إلى ذلك، قدّمت جافا سكريبت حلقتين إضافيتين للتعامل بسهولة مع المجموعات: for...of للتكرار على عناصر مجموعة (مثل المصفوفات أو البنى القابلة للتكرار) حيث يتيح الوصول المباشر لكل عنصر تباعًا، وحلقة for...in للتكرار على مفاتيح كائن (أسماء الخصائص). يجدر استخدام for...of مع المصفوفات ومع البنى القابلة للتكرار، بينما for...in مفيدة عند الحاجة للتعامل مع خصائص كائن.

// مثال على استخدام if/else واستخدام حلقة for
let num = 5;
if (num > 0) {
  console.log("العدد موجب");
} else if (num < 0) {
  console.log("العدد سالب");
} else {
  console.log("العدد صفر");
}

// حلقة من 0 إلى 4
for (let i = 0; i < 5; i++) {
  console.log(i);
}

الدوال (Functions)

الدوال هي كتل من الشيفرة يمكن إعادة استخدامها لتنفيذ مهمة معينة. تسمح لنا بتقسيم البرنامج إلى أجزاء منطقية وإنشاء إجراءات قابلة لإعادة الاستدعاء عدة مرات. في جافا سكريبت، الدوال هي مواطن من الدرجة الأولى (first-class citizens)، مما يعني أنه يمكن التعامل معها كأي قيمة أخرى؛ نستطيع تخزينها في متغيرات، وإرسالها كوسيطات إلى دوال أخرى، وإرجاعها كنتائج من دوال.

يمكن تعريف دالة باستخدام الكلمة المفتاحية function متبوعة باسم الدالة وقائمة الوسائط بين قوسين والجسم ضمن أقواس معقوفة. هذا ما يسمى التصريح عن دالة (Function Declaration). على سبيل المثال: function sum(a, b) { return a + b; } يعرف دالة باسم sum تأخذ وسيطين وتعيد ناتج جمعهما. هناك أيضًا شكل آخر يسمى تعبير الدالة (Function Expression) حيث تعرف الدالة بدون اسم مباشرةً داخل تعبير وإسنادها لمتغير: const sum = function(a, b) { return a + b; };. تختلف التصريحات عن التعبيرات في أن التصريح عن الدالة يتم رفعه (hoisted) بالكامل إلى أعلى نطاقه، مما يتيح استخدامها قبل تعريفها في الكود، بينما التعبير لا يمكن استخدامه إلا بعد تعريفه.

في ES6، تم تقديم الدوال السهمية (Arrow Functions) كصيغة أقصر لكتابة الدوال. تُكتب باستخدام السهم => بدلاً من function. على سبيل المثال: const square = x => x * x; يعرف دالة square تأخذ وسيطًا واحدًا وتعيد مربعه. تتميز الدوال السهمية بأنها تحتفظ بسياق this المحيط بها ولا تملك كائن arguments الخاص بها، مما يجعلها مناسبة للاستخدام كتوابع مختصرة أو عند التعامل مع السياق في البرمجة الكائنية. ومع ذلك، فهي ليست بديلاً كاملًا لكل الحالات؛ فالدوال العادية لا تزال ضرورية عند الحاجة إلى هذا السلوك التقليدي.

يمكن للدوال أن تأخذ معاملات (Parameters) وتمتلك قيمة إرجاع (Return Value). إذا لم تُرجِع الدالة قيمة باستخدام return، فسيكون الناتج ضمنيًا undefined. تدعم جافا سكريبت أيضًا تحديد قيم افتراضية للمعاملات في التعريف: function greet(name = "ضيف") { ... }، واستخدام معامل الانتشار ... لجمع عدد متغير من الوسائط فيما يعرف بـ Rest Parameters (مثال: function sum(...nums) { ... }). تساعد هذه الميزات في جعل الدوال أكثر مرونة. كما يمكن استدعاء دالة داخل أخرى وإنشاء ما يسمى دوال تداخلية (Nested Functions)، مما يقود إلى موضوع الإغلاق (Closure) الذي سنناقشه لاحقًا.

// مثال دالة ترحيب مع قيمة افتراضية + دالة سهمية مكافئة:
function greet(name = "ضيف") {
  return "مرحبًا، " + name;
}
console.log(greet("أحمد")); // "مرحبًا، أحمد"
console.log(greet());       // "مرحبًا، ضيف"

const greetArrow = (name = "ضيف") => `مرحبًا، ${name}`;
console.log(greetArrow("سارة")); // "مرحبًا، سارة"

نطاق المتغيرات والإغلاق (Scope & Closures)

يشير مفهوم النطاق (Scope) إلى المنطقة التي يكون فيها المتغير معرّفًا ومتاحًا للاستخدام في الكود. في جافا سكريبت، لدينا نطاق عام (Global Scope) يشمل المتغيرات المعرفة خارج جميع الدوال، والتي تكون مرئية في أي مكان، ونطاق محلي (Local Scope) خاص بكل دالة أو كتلة. المتغيرات المعرفة داخل دالة باستخدام var تقتصر على تلك الدالة (نطاق دالة)، أما المتغيرات المعرفة داخل كتلة باستخدام let أو const فتقتصر على تلك الكتلة فقط (نطاق كتلة). هذا يعني أنه لا يمكن الوصول إلى متغير محلي من خارج الدالة أو الكتلة التي عُرّف فيها. على سبيل المثال، أي محاولة لاستخدام متغير مُعرّف داخل دالة من خارجها ستؤدي إلى خطأ ReferenceError.

كما ذكرنا سابقًا، المتغيرات المعرّفة بـvar يتم رفعها (hoisted) إلى أعلى نطاق الدالة، بينما let وconst لا ترفع (يُطبّق عليهما ما يسمى منطقة الوقت الميت المؤقتة Temporal Dead Zone حتى يتم تعريفهما). وبالمثل، تعريفات الدوال بالتصريح الكامل (function f() {...}) تُرفع بالكامل أيضًا، مما يتيح استدعاء الدالة قبل تعريفها في الشيفرة، في حين أن تعابير الدوال يجب تعريفها قبل الاستخدام. فهم النطاق مهم جدًا لتفادي التسميات المتضاربة للمتغيرات وللكتابة الصحيحة للكود.

الإغلاق (Closure) هو ميزة قوية في جافا سكريبت تنتج عن نظام النطاق المعجمي (Lexical Scope) في اللغة. يحدث الإغلاق عندما تحتفظ دالة داخلية (مُعرفة داخل دالة أخرى) بالوصول إلى المتغيرات الموجودة في نطاق الدالة الخارجية حتى بعد انتهاء تنفيذ تلك الدالة الخارجية. بكلمات أخرى، الدالة الداخلية تغلق على المتغيرات المحيطة بها لتحفظها. هذا يعني أن المتغيرات المحلية للدالة الخارجية تظل موجودة في الذاكرة طالما استمرت الدالة الداخلية المُغلِقة لها قيد الاستخدام.

لنأخذ مثالاً لتوضيح مفهوم الإغلاق. في الكود أدناه، تقوم الدالة outer بتعريف متغير محلي x ثم تعريف دالة داخلية inner تطبع قيمة x. تقوم outer بإرجاع inner. عند استدعاء outer() والحصول على الدالة inner ثم استدعائها لاحقًا، ستتمكن inner من الوصول إلى المتغير x رغم انتهاء تنفيذ outer، وذلك بفضل الإغلاق:

// مثال 1: دالة داخلية تحتفظ بالوصول لمتغير خارجي بعد انتهاء الدالة الخارجية
function outer() {
  let x = 10;
  function inner() {
    console.log(x);
  }
  return inner;
}

const innerFunc = outer();
innerFunc(); // 10 (لا تزال inner قادرة على رؤية x)

// مثال 2: دالة مولّدة (factory function) تحافظ على حالة داخلية باستخدام الإغلاق
function makeCounter() {
  let count = 0;
  return () => {
    count++;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2 (المتغير count محفوظ داخل نطاق makeCounter لكل استدعاء)

في المثال الأول، حتى بعد انتهاء تنفيذ outer، احتفظت الدالة inner بمرجع إلى المتغير x الموجود في محيطها. وفي المثال الثاني، كل استدعاء للدالة counter يرفع القيمة count بفضل الإغلاق الذي يربطها ببيئتها المغلقة داخل makeCounter. يستخدم المطورون الإغلاق بشكل شائع لإنشاء دوال المصنع (Factory Functions) التي تنتج دوالًا مخصصة ببيانات مخفية، ولتنفيذ نمط المعلومات المخفية (مثل المتغيرات الخاصة) في JavaScript. يُعد فهم الإغلاق ضروريًا لاستيعاب الكثير من أنماط البرمجة في جافا سكريبت، خاصة في تطوير واجهات المستخدم ومعالجة الأحداث.

الكائنات والمصفوفات (Objects & Arrays)

الكائنات في جافا سكريبت هي بنى معطيات تسمح بتخزين مجموعات من القيم كخواص (properties) في هيكل واحد. يتكون الكائن من أزواج اسم الخاصية: القيمة. يمكن إنشاء كائن فارغ باستخدام {} ثم إضافة الخصائص إليه، أو تعريف كائن مع بعض الخصائص مباشرة باستخدام الصيغة الكلية (Object Literal). على سبيل المثال: const person = { name: "علي", age: 25 }; يُعرّف كائنًا باسم person له خاصيتان ابتدائيتان هما name وage. نستطيع الوصول إلى قيم الخصائص باستخدام النقطة (person.name) أو باستخدام المُعامل الأقواسي مع اسم الخاصية كسلسلة (person["name"]).

يمكن إضافة خصائص جديدة أو تعديل الخصائص القائمة في كائن بسهولة. مثلًا: person.job = "مهندس"; يضيف خاصية جديدة job لهذا الكائن. ولحذف خاصية يمكن استخدام معامل delete مثل delete person.age;. يمكن أن تحتوي الكائنات أيضًا على دوال كخصائص (تُسمى أساليب methods). على سبيل المثال، يمكن إضافة دالة sayHello إلى person لتطبع رسالة تحية باستخدام الخصائص الداخلية. داخل الأسلوب، تشير الكلمة المفتاحية this إلى الكائن نفسه، مما يتيح الوصول إلى خصائصه.

تعد المصفوفات (Array) نوعًا خاصًا من الكائنات في جافا سكريبت، وهي عبارة عن قائمة مرتبة من القيم. يتم الوصول إلى عناصر المصفوفة بواسطة الفهارس العددية الخاصة بها (يبدأ العد من 0). يمكن إنشاء مصفوفة باستخدام الأقواس المربعة []، وتستطيع المصفوفة تخزين قيم من أي نوع. تمتلك المصفوفات خواص وأساليب مدمجة مفيدة؛ على سبيل المثال length (لإعطاء عدد العناصر)، وpush (لإضافة عنصر إلى النهاية)، وpop (لحذف آخر عنصر)، وغيرها. مثلًا: const arr = [1, 2, 3]; يعرّف مصفوفة تحتوي ثلاثة أرقام.

// إنشاء كائن person واستخدام خصائصه وأساليبه
const person = { name: "علي", age: 25 };
console.log(person.name);    // "علي"
person.job = "مهندس";
console.log(person["job"]);  // "مهندس"
person.sayHello = function() {
  return `مرحبًا، أنا ${this.name}`;
};
console.log(person.sayHello()); // "مرحبًا، أنا علي"

// إنشاء مصفوفة أرقام وإجراء بعض العمليات عليها
const numbers = [10, 20, 30];
console.log(numbers[1]);   // 20
numbers.push(40);
console.log(numbers.length); // 4
console.log(numbers);      // [10, 20, 30, 40]

رغم أن جافا سكريبت لم تكن تدعم مفهوم الأصناف (Classes) بصورة تقليدية مثل لغات أخرى، إلا أنها توفّر آلية للوراثة بين الكائنات عبر ما يسمى النموذج الأولي (Prototype). هذا يعني أن بإمكان كائن أن يكون له نموذج أولي يرث منه خصائص أو أساليب. ابتداءً من ES6، قدّمت اللغة صيغة class لتسهيل إنشاء الكائنات والوراثة، والتي سنناقشها في القسم التالي ضمن البرمجة كائنية التوجه.

البرمجة كائنية التوجه في جافا سكريبت (Object-Oriented Programming)

تعتمد جافا سكريبت على نموذج الوراثة القائمة على النماذج الأولية (Prototype-based Inheritance) بدلاً من الأصناف الكلاسيكية. يعني ذلك أن كل كائن في جافا سكريبت لديه رابط إلى كائن آخر يُسمى النموذج الأولي (prototype)، ومن خلاله يمكنه وراثة الخصائص والأساليب. إذا حاولنا الوصول إلى خاصية في كائن ولم تكن موجودة فيه مباشرةً، يقوم محرك جافا سكريبت بالبحث عنها في نموذج الكائن الأولي، ثم نموذج النموذج الأولي، وهكذا ضمن سلسلة النموذج الأولي (prototype chain) حتى يصل إلى كائن النموذج الأساسي Object.prototype. بهذه الطريقة، يمكن مشاركة الدوال (الأساليب) بين كائنات متعددة دون تعريفها في كل كائن على حدة.

قبل ES6، كانت الطريقة الأساسية لإنشاء كائنات بنمط كائني هي استخدام الدوال البانية (Constructor Functions) مع خاصية prototype. على سبيل المثال، يمكننا إنشاء دالة بانية لإنشاء كائنات تمثل حيوان:

function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(this.name + " يصدر صوتًا.");
};

const dog = new Animal("كلب");
dog.speak(); // "كلب يصدر صوتًا."

في المثال أعلاه، Animal هي دالة بانية. الخاصية Animal.prototype تحدد أسلوبًا speak سيتم وراثته من قبل جميع الكائنات المُنشأة بواسطة new Animal(...). وبالتالي، الكائن dog لديه القدرة على استدعاء speak رغم أنه لم يُعرَّف مباشرة داخل dog, بل ورثه عبر النموذج الأولي. يمكننا استخدام العامل instanceof للتحقق من نوع الكائن؛ على سبيل المثال dog instanceof Animal ستكون true.

بدءًا من ES6، قدّمت جافا سكريبت صيغة أكثر وضوحًا للتعامل الكائني باستخدام الكلمة المفتاحية class. تتيح لنا الclass تعريف دالة بانية وأساليبها بصورة شبيهة بلغات البرمجة الكائنية الكلاسيكية:

class Animal2 {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + " يصدر صوتًا.");
  }
}

const cat = new Animal2("قط");
cat.speak(); // "قط يصدر صوتًا."

تعريف class Animal2 أعلاه مكافئ في الوظيفة لتعريف function Animal السابق؛ حيث إن كل كائن من نوع Animal2 سيكون لديه خاصية name وسيَرث الأسلوب speak عبر النموذج الأولي للclass. كلمة class في جافا سكريبت هي في الواقع مجرد غلاف تركيبي (syntactic sugar) يستفيد من النموذج الأولي من خلف الكواليس، ولكنه يسهل على المبرمجين كتابة وفهم الكود الكائني.

توفر classes أيضًا ميزة الوراثة (Inheritance) بشكل مباشر باستخدام الكلمة extends. يمكننا إنشاء صنف فرعي (Subclass) يرث من صنف آخر، وإضافة أو تعديل خصائص وأساليب جديدة. مثلًا، يمكننا إنشاء صنف Dog يرث من Animal2:

class Dog extends Animal2 {
  speak() {
    super.speak();
    console.log(this.name + " ينبح.");
  }
}

const rex = new Dog("ريكس");
rex.speak();
// "ريكس يصدر صوتًا."
// "ريكس ينبح."

في هذا المثال، Dog صنف فرعي من Animal2, وقد أعدنا تعريف الأسلوب speak بحيث يستدعي أولًا المنهج الأصلي من الصنف الأب (super.speak()) ثم يضيف سلوكًا إضافيًا (طباعة صوت النباح). عند إنشاء كائن rex من Dog واستدعاء rex.speak() نلاحظ أنه نفّذ السلوك الموروث والجديد معًا. بهذه الطريقة، تسهُل الوراثة وإعادة الاستخدام في الهيكلية الكائنية. من الممكن أيضًا تعريف أساليب وخصائص ثابتة static على الأصناف، وكذلك استخدام خواص الضبط والوصول (getters & setters) لتوفير واجهات محسّنة للكائنات.

البرمجة غير المتزامنة في جافا سكريبت (Asynchronous JavaScript)

على عكس العديد من لغات البرمجة الأخرى، يتم تنفيذ كود جافا سكريبت في بيئة أحادية الخيط (Single-threaded), مما يعني أنه لا يمكن تنفيذ إلا مهمة واحدة في كل مرة. للتعامل مع المهام المعلقة (مثل انتظار استجابة من خادم أو انتظار مؤقت), تستخدم جافا سكريبت وآليات البيئة المضيفة (المتصفح أو Node.js) نموذجًا قائمًا على حلقة الأحداث (Event Loop). تسمح حلقة الأحداث بتنفيذ الكود بشكل غير متزامن (Asynchronously) دون تجميد واجهة المستخدم؛ حيث يتم وضع العمليات المؤجلة في قائمة انتظار (مثل ردود طلبات الشبكة أو أحداث المستخدم) وتتم معالجتها عندما يصبح الخيط الرئيسي فارغًا.

أساس البرمجة غير المتزامنة في جافا سكريبت هو مفهوم استدعاء الرد (Callback). يمكن تمرير دالة كوسيط إلى دالة أخرى لتُستدعى لاحقًا عند اكتمال مهمة معينة. على سبيل المثال، setTimeout دالة مجّهزة تؤجل تنفيذ دالة معينة لمدة زمنية، وaddEventListener يستخدم دالة رد نمررها ليتم استدعاؤها عند وقوع حدث معين. هذا الأسلوب شائع جدًا في التعامل مع مهام مثل طلبات AJAX (الاتصال بالخادم) أو معالجة أحداث المستخدم. ومع ذلك، الاعتماد المكثف على التوابع المتداخلة (callbacks المتداخلة) قد يقود إلى ما يسمى “جحيم الاستدعاءات المتداخلة” (Callback Hell) الذي يصعّب قراءة وصيانة الشيفرة.

لتسهيل التعامل مع التدفق غير المتزامن وتفادي التعشيش المعقد للاستدعاءات، قدمت ES6 ميزة الوعود (Promises). الوعد هو كائن يمثل نتيجة عملية غير متزامنة التي قد تكتمل في المستقبل (بالنجاح أو بالفشل). يوفّر الوعد واجهات .then() و.catch() للتعامل مع النتيجة عند اكتمالها أو وقوع خطأ، مما يتيح تنظيمًا أوضح للتسلسل المنطقي مقارنةً بالاستدعاءات المتداخلة. باستخدام الوعود، يمكن كتابة سلسلة من العمليات غير المتزامنة بطريقة أشبه بالتسلسل المتزامن.

وفي ES2017 (ES8)، تمت إضافة صياغة async/await كطبقة إضافية من التجريد فوق الوعود. تسمح الكلمة المفتاحية async بتعريف دالة غير متزامنة تُرجع ضمنيًا وعدًا (Promise)، وتستخدم await داخلها لانتظار نتيجة وعد آخر بشكل يبدو متزامنًا. هذا يجعل الكود غير المتزامن يبدو ويتصرف بشكل تسلسلي أكثر سهولة للفهم. تحت الغطاء، لا تزال الوعود وحلقة الأحداث هي ما يدير الأمور، لكن المطور لم يعد بحاجة للتعامل المباشر مع .then و.catch في كل مرة.

لفهم كيفية عمل حلقة الأحداث، انظر المثال التالي الذي يستخدم setTimeout:

console.log("Start");
setTimeout(() => {
  console.log("Timeout");
}, 1000);
console.log("End");
// الترتيب المتوقع في وحدة التحكم:
// Start
// End
// (بعد 1 ثانية) Timeout

في المثال أعلاه، نرى أن الشيفرة غير المتزامنة (داخل setTimeout) لم تمنع استمرار تنفيذ بقية الكود؛ تمّت طباعة “Start” ثم “End” مباشرةً، ثم بعد انقضاء التأخير ظهرت “Timeout”. يحدث هذا لأن دالة setTimeout تدفع الدالة الممررة لها إلى قائمة الانتظار لتنفذ لاحقًا، مما يسمح لوحدة المعالجة باستكمال المهام التالية فورًا.

يظهر مفهوم الوعود وasync/await في المثال التالي:

function wait(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log("Waiting...");
  await wait(1000);
  console.log("Done");
}

run();
// في وحدة التحكم:
// Waiting...
// (بعد 1 ثانية) Done

لدينا هنا دالة wait تُرجع وعدًا يقوم ببساطة بتأخير التنفيذ لمدة معينة (باستخدام setTimeout داخل promise). ثم قمنا بتعريف دالة async اسمها run تنتظر (await) اكتمال wait(1000) قبل طباعة “Done”. عند استدعاء run()، تطبع “Waiting…” فورًا، ثم تنتظر ثانية واحدة (دون حجب التنفيذ العام) قبل طباعة “Done”. خلال فترة الانتظار، يكون مؤشر التنفيذ قد عاد إلى حلقة الأحداث لمعالجة مهام أخرى إن وجدت. بهذه الطريقة، يوفر async/await أسلوبًا واضحًا لكتابة الكود غير المتزامن كما لو كان متزامنًا، مع الاحتفاظ بكفاءة التنفيذ غير المتزامن في الخلفية.

لقد حاولنا في هذه المقالة تغطية أهم أساسيات جافا سكريبت وبعض المواضيع المتقدمة بشكل مبسّط وشامل. بطبيعة الحال، عالم جافا سكريبت أوسع بكثير؛ فهناك مزيد من الجوانب لاستكشافها مثل التفاعل مع صفحة الويب عبر DOM (واجهة برمجة المستند) وBOM (نموذج كائن المتصفح)، والتعامل مع وحدات ES6 (Modules)، وغيرها الكثير. كما تظهر ميزات جديدة للغة باستمرار مع تطور المواصفات. أفضل طريقة لإتقان جافا سكريبت هي بالممارسة المستمرة وقراءة التوثيقات الرسمية والاستفادة من المصادر التعليمية المتاحة.

مصادر إضافية للتعلم

التعليقات

اترك تعليقاً