إنشاء الكائنات في JavaScript: دليل شامل للمطورين لاستكشاف Object Literal, Object.create, Classes, ودوال المصنع

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

مقدمة إلى الكائنات في JavaScript

تُعد الكائنات الوحدة الأساسية للتغليف في البرمجة الشيئية (Object-Oriented Programming). في عالم JavaScript المرن والديناميكي، هناك عدة طرق لإنشاء هذه الكائنات وإدارتها، كل منها يقدم مزايا وحالات استخدام فريدة. يهدف هذا المقال إلى استكشاف هذه الطرق بالتفصيل، مما يوفر لك فهمًا عميقًا لكيفية بناء الكائنات بكفاءة في مشاريعك.

سنغطي الطرق التالية لإنشاء الكائنات:

  • Object Literal (الكائن الحرفي)
  • Object.create()
  • Classes (الفئات)
  • Factory Functions (دوال المصنع)

1. الكائن الحرفي (Object Literal): البساطة والفعالية

قبل الخوض في تفاصيل الكائنات، من المهم التمييز بين هياكل البيانات والكائنات الموجهة نحو الكائنات (Object-Oriented Objects). هياكل البيانات عادةً ما تحتوي على بيانات عامة ولا تتضمن سلوكًا (أي لا تحتوي على دوال/methods). يمكننا بسهولة إنشاء مثل هذه الكائنات باستخدام صيغة الكائن الحرفي، والتي تتميز بالبساطة والوضوح.

تبدو صيغة الكائن الحرفي كما يلي:

const product = {
  name: 'apple',
  category: 'fruits',
  price: 1.99
}
console.log(product);

في JavaScript، الكائنات هي مجموعات ديناميكية من أزواج المفتاح والقيمة (key-value pairs). يكون المفتاح دائمًا سلسلة نصية (string) ويجب أن يكون فريدًا داخل الكائن. أما القيمة، فيمكن أن تكون قيمة بدائية (primitive)، أو كائنًا آخر، أو حتى دالة (function).

يمكننا الوصول إلى خاصية معينة باستخدام طريقة النقطة (dot notation) أو طريقة الأقواس المربعة (square bracket notation):

console.log(product.name); //"apple"
console.log(product["name"]); //"apple"

الكائنات المتداخلة (Nested Objects)

يمكن أن تكون قيمة الخاصية كائنًا آخر، مما يتيح لنا بناء هياكل بيانات معقدة ومتداخلة. إليك مثال حيث تكون القيمة كائنًا آخر:

const product = {
  name: 'apple',
  category: 'fruits',
  price: 1.99,
  nutrients: {
    carbs: 0.95,
    fats: 0.3,
    protein: 0.2
  }
}

في هذا المثال، قيمة الخاصية nutrients هي كائن جديد. يمكننا الوصول إلى خاصية متداخلة مثل carbs بالطريقة التالية:

console.log(product.nutrients.carbs); //0.95

أسماء الخصائص المختصرة (Shorthand Property Names)

لنفترض أن لدينا قيم خصائصنا مخزنة في متغيرات منفصلة:

const name = 'apple';
const category = 'fruits';
const price = 1.99;

const product = {
  name: name,
  category: category,
  price: price
}

تدعم JavaScript ما يسمى بـ shorthand property names، والذي يسمح لنا بإنشاء كائن باستخدام اسم المتغير فقط، وسيقوم بإنشاء خاصية بنفس الاسم. الكائن الحرفي التالي مكافئ تمامًا للكائن السابق:

const name = 'apple';
const category = 'fruits';
const price = 1.99;

const product = {
  name,
  category,
  price
}

2. استخدام Object.create(): الوراثة القائمة على النموذج الأولي (Prototype-based Inheritance)

لننتقل الآن إلى كيفية تطبيق الكائنات ذات السلوك (behavior)، أي الكائنات الموجهة نحو الكائنات الحقيقية. تمتلك JavaScript ما يسمى بنظام النموذج الأولي (prototype system) الذي يسمح بمشاركة السلوك بين الكائنات. الفكرة الرئيسية هي إنشاء كائن يُسمى prototype (النموذج الأولي) يحتوي على سلوك مشترك، ثم استخدامه عند إنشاء كائنات جديدة. يتيح لنا هذا النظام إنشاء كائنات ترث السلوك من كائنات أخرى.

لنقم بإنشاء كائن نموذج أولي يسمح لنا بإضافة منتجات والحصول على السعر الإجمالي من سلة التسوق:

const cartPrototype = {
  addProduct: function (product) {
    if (!this.products) {
      this.products = [product]
    } else {
      this.products.push(product);
    }
  },
  getTotalPrice: function () {
    return this.products.reduce((total, p) => total + p.price, 0);
  }
}

لاحظ أن قيمة الخاصية addProduct هذه المرة هي دالة. يمكننا أيضًا كتابة الكائن السابق باستخدام شكل أقصر يُسمى shorthand method syntax:

const cartPrototype = {
  addProduct(product) { /* code */ },
  getTotalPrice() { /* code */ }
}

يُعد cartPrototype هو الكائن النموذجي الذي يحمل السلوك المشترك المتمثل في دالتين: addProduct وgetTotalPrice. يمكن استخدامه لبناء كائنات أخرى ترث هذا السلوك.

const cart = Object.create(cartPrototype);
cart.addProduct({ name: 'orange', price: 1.25 });
cart.addProduct({ name: 'lemon', price: 1.75 });
console.log(cart.getTotalPrice()); //3

الكائن cart لديه cartPrototype كنموذج أولي له، ويرث السلوك منه. يحتوي cart على خاصية مخفية تشير إلى الكائن النموذجي. عندما نستخدم دالة (method) على كائن، يتم البحث عن هذه الدالة أولاً في الكائن نفسه، وليس في نموذجه الأولي.

الكلمة المفتاحية this

لاحظ أننا نستخدم كلمة مفتاحية خاصة تُسمى this للوصول إلى البيانات وتعديلها داخل الكائن. تذكر أن الدوال في JavaScript هي وحدات سلوك مستقلة، وليست بالضرورة جزءًا من كائن. عندما تكون جزءًا من كائن، نحتاج إلى مرجع يسمح للدالة بالوصول إلى الأعضاء الآخرين في نفس الكائن. هنا يأتي دور this، فهو يمثل سياق الدالة (function context) ويوفر الوصول إلى الخصائص الأخرى.

البيانات في النماذج الأولية: تجنب الأخطاء الشائعة

قد تتساءل لماذا لم نقم بتعريف وتهيئة الخاصية products على كائن النموذج الأولي نفسه. يجب ألا نفعل ذلك. يجب استخدام النماذج الأولية لمشاركة السلوك (behavior)، وليس البيانات (data). ستؤدي مشاركة البيانات إلى وجود نفس المنتجات في عدة كائنات سلة تسوق، وهو ما لا نريده.

تأمل الكود أدناه:

const cartPrototype = {
  products: [],
  addProduct: function (product) {
    this.products.push(product);
  },
  getTotalPrice: function () { /* code */ }
}

const cart1 = Object.create(cartPrototype);
cart1.addProduct({ name: 'orange', price: 1.25 });
cart1.addProduct({ name: 'lemon', price: 1.75 });
console.log(cart1.getTotalPrice()); //3

const cart2 = Object.create(cartPrototype);
console.log(cart2.getTotalPrice()); //3

في هذا المثال، كلا الكائنين cart1 وcart2 يرثان السلوك المشترك من cartPrototype، ولكنهما أيضًا يشتركان في نفس البيانات (مصفوفة products). هذا ليس السلوك المطلوب. يجب استخدام النماذج الأولية لمشاركة السلوك، وليس البيانات.

3. الفئات (Classes): تبسيط الوراثة القائمة على النموذج الأولي

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

توفر الفئة مكانًا محددًا لتعريف البيانات المميزة لكل كائن. إليك نفس الكائن الذي تم إنشاؤه باستخدام صيغة الفئة (class sugar syntax):

class Cart {
  constructor() {
    this.products = [];
  }

  addProduct(product) {
    this.products.push(product);
  }

  getTotalPrice() {
    return this.products.reduce((total, p) => total + p.price, 0);
  }
}

const cart = new Cart();
cart.addProduct({ name: 'orange', price: 1.25 });
cart.addProduct({ name: 'lemon', price: 1.75 });
console.log(cart.getTotalPrice()); //3

const cart2 = new Cart();
console.log(cart2.getTotalPrice()); //0

لاحظ أن الفئة تحتوي على دالة constructor (الباني) التي تقوم بتهيئة البيانات المميزة لكل كائن جديد. البيانات داخل الباني لا يتم مشاركتها بين النسخ (instances). لإنشاء نسخة جديدة، نستخدم الكلمة المفتاحية new. تُعد صيغة الفئة أكثر وضوحًا وألفة لمعظم المطورين، ومع ذلك، فإنها تقوم بعمل مشابه: تنشئ نموذجًا أوليًا بجميع الدوال وتستخدمه لتعريف كائنات جديدة. يمكن الوصول إلى النموذج الأولي عبر Cart.prototype. يتبين أن نظام النموذج الأولي مرن بما يكفي للسماح بصيغة الفئة، مما يعني أنه يمكن محاكاة نظام الفئة باستخدام نظام النموذج الأولي.

الخصائص الخاصة (Private Properties)

الشيء الوحيد هو أن الخاصية products في الكائن الجديد تكون عامة (public) بشكل افتراضي:

console.log(cart.products); //[{name: "orange", price: 1.25}, {name: "lemon", price: 1.75}]

يمكننا جعلها خاصة باستخدام البادئة # (hash prefix). تُعلن الخصائص الخاصة باستخدام صيغة #name. تُعد # جزءًا من اسم الخاصية نفسه ويجب استخدامها عند الإعلان عن الخاصية والوصول إليها. إليك مثال على الإعلان عن products كخاصية خاصة:

class Cart {
  #products; // إعلان الخاصية الخاصة

  constructor() {
    this.#products = [];
  }

  addProduct(product) {
    this.#products.push(product);
  }

  getTotalPrice() {
    return this.#products.reduce((total, p) => total + p.price, 0);
  }
}

// عند محاولة الوصول إليها من الخارج، ستحدث خطأ:
// const cart = new Cart();
// console.log(cart.#products); // Uncaught SyntaxError: Private field '#products' must be declared in an enclosing class

كما ترى في التعليق، محاولة الوصول إلى #products من خارج الفئة ستؤدي إلى خطأ SyntaxError، مما يضمن خصوصية البيانات.

4. دوال المصنع (Factory Functions): الكائنات كمجموعات من الإغلاقات (Closures)

خيار آخر لإنشاء الكائنات هو بناؤها كمجموعات من الإغلاقات (closures). الإغلاق هو قدرة الدالة على الوصول إلى المتغيرات والمعاملات من الدالة الخارجية، حتى بعد انتهاء تنفيذ الدالة الخارجية. لنلقِ نظرة على كائن cart الذي تم بناؤه باستخدام ما يسمى بدالة المصنع (factory function):

function Cart() {
  const products = []; // هذا المتغير خاص بالدالة الخارجية

  function addProduct(product) {
    products.push(product);
  }

  function getTotalPrice() {
    return products.reduce((total, p) => total + p.price, 0);
  }

  return {
    addProduct,
    getTotalPrice
  }
}

const cart = Cart();
cart.addProduct({ name: 'orange', price: 1.25 });
cart.addProduct({ name: 'lemon', price: 1.75 });
console.log(cart.getTotalPrice()); //3

في هذا المثال، addProduct وgetTotalPrice هما دالتان داخليتان تصلان إلى المتغير products من دالتهما الأم (Cart). لديهما وصول إلى متغير products حتى بعد انتهاء تنفيذ الدالة الأم Cart. تُعد addProduct وgetTotalPrice إغلاقين (closures) يشتركان في نفس المتغير الخاص products.

Cart هي دالة مصنع. الكائن الجديد cart الذي تم إنشاؤه باستخدام دالة المصنع لديه متغير products خاص (private). لا يمكن الوصول إليه من الخارج:

console.log(cart.products); //undefined

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

ملخص طرق إنشاء الكائنات

عادةً ما نتعامل مع نوعين من الكائنات: هياكل البيانات التي تحتوي على بيانات عامة ولا سلوك، والكائنات الموجهة نحو الكائنات التي تحتوي على بيانات خاصة وسلوك عام. يمكن بناء هياكل البيانات بسهولة باستخدام صيغة الكائن الحرفي (object literal syntax).

تقدم JavaScript طريقتين مبتكرتين لإنشاء الكائنات الموجهة نحو الكائنات:

  1. **نظام النموذج الأولي (Prototype System):** باستخدام كائن نموذج أولي لمشاركة السلوك المشترك. الكائنات ترث من كائنات أخرى. توفر الفئات (Classes) صيغة مختصرة وجذابة لإنشاء مثل هذه الكائنات، مع معالجة مشكلة مشاركة البيانات بفعالية.
  2. **الإغلاقات (Closures):** عن طريق تعريف الكائنات كمجموعات من الإغلاقات (دوال المصنع). هذه الطريقة توفر خصوصية للبيانات بشكل طبيعي دون الحاجة إلى الكلمات المفتاحية مثل this أو new.

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

يُظهر استكشاف طرق إنشاء الكائنات في JavaScript مرونة اللغة وقوتها في التعامل مع النماذج البرمجية المختلفة. بينما يوفر Object Literal طريقة سريعة ومباشرة لتعريف هياكل البيانات، فإن Object.create() يكشف عن جوهر الوراثة القائمة على النموذج الأولي، مؤكدًا على أهمية فصل السلوك عن البيانات المشتركة. تُعد Classes بمثابة طبقة تجريدية أنيقة فوق هذا النظام، مما يجعل البرمجة الشيئية أكثر سهولة وألفة للمطورين، خاصة مع إضافة الخصائص الخاصة (#private) التي تعزز التغليف. من ناحية أخرى، تقدم دوال المصنع (Factory Functions) مدخلاً وظيفيًا قويًا، مستفيدة من مفهوم الإغلاقات لتحقيق خصوصية البيانات بطريقة طبيعية وبسيطة، مما يجعلها خيارًا ممتازًا للحالات التي تتطلب كائنات ذات حالة داخلية محمية دون تعقيدات this أو new. اختيار الطريقة الأنسب يعتمد على متطلبات المشروع، أسلوب البرمجة المفضل، ومستوى التحكم المطلوب في الوراثة وخصوصية البيانات.

اترك تعليقاً

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