نظرة معمقة على سلسلة النماذج الأولية في جافاسكريبت، واستكشاف دورها الأساسي في إنشاء الكائنات وأنماط الوراثة لجمهور عالمي.
كشف سلسلة النماذج الأولية (Prototype Chain) في جافاسكريبت: أنماط الوراثة وإنشاء الكائنات
تعد جافاسكريبت، في جوهرها، لغة ديناميكية ومتعددة الاستخدامات دعمت الويب لعقود. بينما يألف العديد من المطورين جوانبها الوظيفية وصيغتها الحديثة التي تم تقديمها في ECMAScript 6 (ES6) وما بعده، فإن فهم آلياتها الأساسية أمر بالغ الأهمية لإتقان اللغة حقًا. أحد المفاهيم الأساسية والتي غالبًا ما يساء فهمها هو سلسلة النماذج الأولية (prototype chain). سيوضح هذا المقال سلسلة النماذج الأولية، مستكشفًا كيفية تسهيلها لإنشاء الكائنات وتمكينها لأنماط الوراثة المختلفة، مما يوفر منظورًا عالميًا للمطورين في جميع أنحاء العالم.
الأساس: الكائنات والخصائص في جافاسكريبت
قبل الغوص في سلسلة النماذج الأولية، دعونا نؤسس فهمًا أساسيًا لكيفية عمل الكائنات في جافاسكريبت. في جافاسكريبت، كل شيء تقريبًا هو كائن. الكائنات هي مجموعات من أزواج المفاتيح والقيم، حيث تكون المفاتيح أسماء الخصائص (عادةً ما تكون سلاسل نصية أو رموز Symbols) ويمكن أن تكون القيم أي نوع من البيانات، بما في ذلك الكائنات الأخرى، أو الوظائف، أو القيم الأولية.
لنأخذ كائنًا بسيطًا كمثال:
const person = {
name: "Alice",
age: 30,
greet: function() {
console.log(`Hello, my name is ${this.name}.`);
}
};
console.log(person.name); // Output: Alice
person.greet(); // Output: Hello, my name is Alice.
عند الوصول إلى خاصية لكائن ما، مثل person.name، تبحث جافاسكريبت أولاً عن تلك الخاصية مباشرة في الكائن نفسه. إذا لم تجدها، فإنها لا تتوقف عند هذا الحد. هنا يأتي دور سلسلة النماذج الأولية.
ما هو النموذج الأولي (Prototype)؟
لكل كائن في جافاسكريبت خاصية داخلية، يشار إليها غالبًا بـ [[Prototype]]، والتي تشير إلى كائن آخر. يسمى هذا الكائن الآخر النموذج الأولي (prototype) للكائن الأصلي. عندما تحاول الوصول إلى خاصية في كائن ما ولا يتم العثور عليها مباشرة في الكائن، تبحث جافاسكريبت عنها في النموذج الأولي للكائن. إذا لم يتم العثور عليها هناك، فإنها تبحث في النموذج الأولي للنموذج الأولي، وهكذا، مكونةً سلسلة.
تستمر هذه السلسلة حتى تجد جافاسكريبت الخاصية أو تصل إلى نهاية السلسلة، والتي تكون عادةً Object.prototype، الذي تكون خاصيته [[Prototype]] هي null. تُعرف هذه الآلية باسم الوراثة القائمة على النموذج الأولي (prototypal inheritance).
الوصول إلى النموذج الأولي
على الرغم من أن [[Prototype]] هي خانة داخلية، إلا أن هناك طريقتين أساسيتين للتفاعل مع النموذج الأولي للكائن:
Object.getPrototypeOf(obj): هذه هي الطريقة القياسية والموصى بها للحصول على النموذج الأولي للكائن.obj.__proto__: هذه خاصية مهملة ولكنها مدعومة على نطاق واسع وغير قياسية وتعيد أيضًا النموذج الأولي. يُنصح عمومًا باستخدامObject.getPrototypeOf()لتحسين التوافق والالتزام بالمعايير.
const person = {
name: "Alice"
};
const personPrototype = Object.getPrototypeOf(person);
console.log(personPrototype === Object.prototype); // Output: true
// Using the deprecated __proto__
console.log(person.__proto__ === Object.prototype); // Output: true
سلسلة النماذج الأولية أثناء العمل
سلسلة النماذج الأولية هي في الأساس قائمة مرتبطة من الكائنات. عندما تحاول الوصول إلى خاصية (للحصول عليها أو تعيينها أو حذفها)، تجتاز جافاسكريبت هذه السلسلة:
- تتحقق جافاسكريبت مما إذا كانت الخاصية موجودة مباشرة في الكائن نفسه.
- إذا لم يتم العثور عليها، فإنها تتحقق من النموذج الأولي للكائن (
obj.[[Prototype]]). - إذا لم يتم العثور عليها بعد، فإنها تتحقق من النموذج الأولي للنموذج الأولي، وهكذا.
- يستمر هذا حتى يتم العثور على الخاصية أو تنتهي السلسلة عند كائن يكون نموذجه الأولي
null(عادةًObject.prototype).
دعنا نوضح بمثال. تخيل أن لدينا دالة بانية أساسية `Animal` ثم دالة بانية `Dog` ترث من `Animal`.
// دالة بانية لـ Animal
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
// دالة بانية لـ Dog
function Dog(name, breed) {
Animal.call(this, name); // استدعاء الدالة البانية الأب
this.breed = breed;
}
// إعداد سلسلة النماذج الأولية: Dog.prototype يرث من Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // تصحيح خاصية constructor
Dog.prototype.bark = function() {
console.log(`Woof! My name is ${this.name} and I'm a ${this.breed}.`);
};
const myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.name); // Output: Buddy (found on myDog)
myDog.speak(); // Output: Buddy makes a sound. (found on Dog.prototype via Animal.prototype)
myDog.bark(); // Output: Woof! My name is Buddy and I'm a Golden Retriever. (found on Dog.prototype)
console.log(Object.getPrototypeOf(myDog) === Dog.prototype); // Output: true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // Output: true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // Output: true
console.log(Object.getPrototypeOf(Object.prototype) === null); // Output: true
في هذا المثال:
- يمتلك
myDogخاصية مباشرةnameوbreed. - عند استدعاء
myDog.speak()، تبحث جافاسكريبت عنspeakفيmyDog. لا يتم العثور عليها. - ثم تبحث في
Object.getPrototypeOf(myDog)، وهوDog.prototype. لا يتم العثور علىspeakهناك. - ثم تبحث في
Object.getPrototypeOf(Dog.prototype)، وهوAnimal.prototype. هنا، يتم العثور علىspeak! يتم تنفيذ الدالة، وthisداخلspeakتشير إلىmyDog.
أنماط إنشاء الكائنات
ترتبط سلسلة النماذج الأولية ارتباطًا جوهريًا بكيفية إنشاء الكائنات في جافاسكريبت. تاريخيًا، قبل فئات ES6، تم استخدام عدة أنماط لتحقيق إنشاء الكائنات والوراثة:
1. الدوال البانية (Constructor Functions)
كما رأينا في أمثلة Animal و Dog أعلاه، تعد الدوال البانية طريقة تقليدية لإنشاء الكائنات. عند استخدام الكلمة المفتاحية new مع دالة، تقوم جافاسكريبت بتنفيذ عدة إجراءات:
- يتم إنشاء كائن فارغ جديد.
- يتم ربط هذا الكائن الجديد بخاصية
prototypeللدالة البانية (أيnewObj.[[Prototype]] = Constructor.prototype). - يتم استدعاء الدالة البانية مع ربط الكائن الجديد بـ
this. - إذا لم تُعد الدالة البانية كائنًا بشكل صريح، فسيتم إرجاع الكائن الذي تم إنشاؤه حديثًا (
this) ضمنيًا.
هذا النمط قوي لإنشاء مثيلات متعددة من الكائنات ذات الأساليب المشتركة المحددة في النموذج الأولي للدالة البانية.
2. الدوال المصنعية (Factory Functions)
الدوال المصنعية هي ببساطة دوال تعيد كائنًا. لا تستخدم الكلمة المفتاحية new ولا ترتبط تلقائيًا بنموذج أولي بنفس طريقة الدوال البانية. ومع ذلك، لا يزال بإمكانها الاستفادة من النماذج الأولية عن طريق تعيين النموذج الأولي للكائن المُعاد بشكل صريح.
function createPerson(name, age) {
const person = Object.create(personFactory.prototype);
person.name = name;
person.age = age;
return person;
}
personFactory.prototype.greet = function() {
console.log(`Hello, I'm ${this.name}`);
};
const john = createPerson("John", 25);
john.greet(); // Output: Hello, I'm John
تُعد Object.create() طريقة رئيسية هنا. فهي تنشئ كائنًا جديدًا، باستخدام كائن موجود كنموذج أولي للكائن الذي تم إنشاؤه حديثًا. وهذا يسمح بالتحكم الصريح في سلسلة النماذج الأولية.
3. `Object.create()`
كما أشرنا أعلاه، تعد Object.create(proto, [propertiesObject]) أداة أساسية لإنشاء كائنات بنموذج أولي محدد. تسمح لك بتجاوز الدوال البانية بالكامل وتعيين النموذج الأولي للكائن مباشرة.
const personPrototype = {
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};
// إنشاء كائن جديد 'bob' مع 'personPrototype' كنموذج أولي له
const bob = Object.create(personPrototype);
bob.name = "Bob";
bob.greet(); // Output: Hello, my name is Bob
// يمكنك حتى تمرير الخصائص كوسيط ثانٍ
const charles = Object.create(personPrototype, {
name: { value: "Charles", writable: true, enumerable: true, configurable: true }
});
charles.greet(); // Output: Hello, my name is Charles
هذه الطريقة قوية للغاية لإنشاء كائنات ذات نماذج أولية محددة مسبقًا، مما يتيح هياكل وراثة مرنة.
فئات ES6: سكر نحوي (Syntactic Sugar)
مع ظهور ES6، قدمت جافاسكريبت صيغة class. من المهم أن نفهم أن الفئات في جافاسكريبت هي في المقام الأول سكر نحوي (syntactic sugar) فوق آلية الوراثة القائمة على النموذج الأولي الحالية. إنها توفر صيغة أنظف وأكثر ألفة للمطورين القادمين من لغات البرمجة كائنية التوجه القائمة على الفئات.
// استخدام صيغة فئة ES6
class AnimalES6 {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class DogES6 extends AnimalES6 {
constructor(name, breed) {
super(name); // يستدعي الدالة البانية للفئة الأب
this.breed = breed;
}
bark() {
console.log(`Woof! My name is ${this.name} and I'm a ${this.breed}.`);
}
}
const myDogES6 = new DogES6("Rex", "German Shepherd");
myDogES6.speak(); // Output: Rex makes a sound.
myDogES6.bark(); // Output: Woof! My name is Rex and I'm a German Shepherd.
// تحت الغطاء، لا يزال هذا يستخدم النماذج الأولية:
console.log(Object.getPrototypeOf(myDogES6) === DogES6.prototype); // Output: true
console.log(Object.getPrototypeOf(DogES6.prototype) === AnimalES6.prototype); // Output: true
عندما تحدد فئة، تقوم جافاسكريبت بشكل أساسي بإنشاء دالة بانية وإعداد سلسلة النماذج الأولية تلقائيًا:
- تحدد طريقة
constructorخصائص مثيل الكائن. - يتم وضع الطرق المحددة داخل جسم الفئة (مثل
speakوbark) تلقائيًا على خاصيةprototypeللدالة البانية المرتبطة بتلك الفئة. - تؤسس الكلمة المفتاحية
extendsعلاقة الوراثة، وتربط النموذج الأولي للفئة الفرعية بالنموذج الأولي للفئة الأصل.
لماذا تهم سلسلة النماذج الأولية عالميًا
إن فهم سلسلة النماذج الأولية ليس مجرد تمرين أكاديمي؛ فله آثار عميقة على تطوير تطبيقات جافاسكريبت قوية وفعالة وقابلة للصيانة، خاصة في سياق عالمي:
- تحسين الأداء: من خلال تحديد الأساليب على النموذج الأولي بدلاً من كل مثيل كائن على حدة، فإنك توفر الذاكرة. تشترك جميع المثيلات في نفس دوال الأساليب، مما يؤدي إلى استخدام أكثر كفاءة للذاكرة، وهو أمر بالغ الأهمية للتطبيقات المنشورة على مجموعة واسعة من الأجهزة وظروف الشبكة في جميع أنحاء العالم.
- إعادة استخدام الكود: سلسلة النماذج الأولية هي الآلية الأساسية في جافاسكريبت لإعادة استخدام الكود. تسمح لك الوراثة ببناء تسلسلات هرمية معقدة للكائنات، وتوسيع الوظائف دون تكرار الكود. هذا أمر لا يقدر بثمن للفرق الكبيرة والموزعة التي تعمل على مشاريع دولية.
- التصحيح العميق: عند حدوث أخطاء، يمكن أن يساعد تتبع سلسلة النماذج الأولية في تحديد مصدر السلوك غير المتوقع. يعد فهم كيفية البحث عن الخصائص أمرًا أساسيًا لتصحيح المشكلات المتعلقة بالوراثة والنطاق وربط
this. - أطر العمل والمكتبات: تعتمد العديد من أطر عمل ومكتبات جافاسكريبت الشهيرة (مثل الإصدارات الأقدم من React و Angular و Vue.js) بشكل كبير على سلسلة النماذج الأولية أو تتفاعل معها. يساعدك الفهم القوي للنماذج الأولية على فهم طريقة عملها الداخلية واستخدامها بشكل أكثر فعالية.
- التوافقية بين اللغات: مرونة جافاسكريبت مع النماذج الأولية تجعل من السهل دمجها مع أنظمة أو لغات أخرى، خاصة في بيئات مثل Node.js حيث تتفاعل جافاسكريبت مع الوحدات النمطية الأصلية.
- الوضوح المفاهيمي: بينما تخفي فئات ES6 بعض التعقيدات، فإن الفهم الأساسي للنماذج الأولية يسمح لك بفهم ما يحدث تحت الغطاء. هذا يعمق فهمك ويمكّنك من التعامل مع الحالات الهامشية والسيناريوهات المتقدمة بثقة أكبر، بغض النظر عن موقعك الجغرافي أو بيئة التطوير المفضلة لديك.
المزالق الشائعة وأفضل الممارسات
على الرغم من قوتها، يمكن أن تؤدي سلسلة النماذج الأولية أيضًا إلى الارتباك إذا لم يتم التعامل معها بعناية. فيما يلي بعض المزالق الشائعة وأفضل الممارسات:
المأزق الأول: تعديل النماذج الأولية المدمجة
من السيء عمومًا إضافة أو تعديل الأساليب على النماذج الأولية للكائنات المدمجة مثل Array.prototype أو Object.prototype. يمكن أن يؤدي ذلك إلى تعارض في التسمية وسلوك غير متوقع، خاصة في المشاريع الكبيرة أو عند استخدام مكتبات تابعة لجهات خارجية قد تعتمد على السلوك الأصلي لهذه النماذج الأولية.
أفضل ممارسة: استخدم الدوال البانية الخاصة بك، أو الدوال المصنعية، أو فئات ES6. إذا كنت بحاجة إلى توسيع الوظائف، ففكر في إنشاء دوال مساعدة أو استخدام الوحدات النمطية.
المأزق الثاني: خاصية `constructor` غير صحيحة
عند إعداد الوراثة يدويًا (على سبيل المثال، Dog.prototype = Object.create(Animal.prototype))، ستشير خاصية constructor للنموذج الأولي الجديد (Dog.prototype) إلى الدالة البانية الأصلية (Animal). يمكن أن يسبب هذا مشاكل في عمليات التحقق باستخدام `instanceof` والفحص الداخلي.
أفضل ممارسة: قم دائمًا بإعادة تعيين خاصية constructor بشكل صريح بعد إعداد الوراثة:
Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog;
المأزق الثالث: فهم سياق `this`
سلوك this داخل أساليب النموذج الأولي أمر بالغ الأهمية. تشير this دائمًا إلى الكائن الذي تم استدعاء الطريقة عليه، وليس حيث تم تعريف الطريقة. هذا أساسي لكيفية عمل الأساليب عبر سلسلة النماذج الأولية.
أفضل ممارسة: كن على دراية بكيفية استدعاء الأساليب. استخدم `.call()` أو `.apply()` أو `.bind()` إذا كنت بحاجة إلى تعيين سياق `this` بشكل صريح، خاصة عند تمرير الأساليب كدوال استدعاء.
المأزق الرابع: الخلط مع الفئات في اللغات الأخرى
قد يجد المطورون المعتادون على الوراثة الكلاسيكية (مثل جافا أو C++) نموذج الوراثة القائم على النموذج الأولي في جافاسكريبت غير بديهي في البداية. تذكر أن فئات ES6 هي واجهة؛ الآلية الأساسية لا تزال هي النماذج الأولية.
أفضل ممارسة: احتضن طبيعة جافاسكريبت القائمة على النموذج الأولي. ركز على فهم كيفية تفويض الكائنات لعمليات البحث عن الخصائص من خلال نماذجها الأولية.
ما وراء الأساسيات: مفاهيم متقدمة
عامل `instanceof`
يتحقق عامل instanceof مما إذا كانت سلسلة النماذج الأولية للكائن تحتوي على خاصية prototype لدالة بانية معينة. إنها أداة قوية للتحقق من النوع في نظام قائم على النموذج الأولي.
console.log(myDog instanceof Dog); // Output: true console.log(myDog instanceof Animal); // Output: true console.log(myDog instanceof Object); // Output: true console.log(myDog instanceof Array); // Output: false
طريقة `isPrototypeOf()`
تتحقق طريقة Object.prototype.isPrototypeOf() مما إذا كان كائن ما يظهر في أي مكان في سلسلة النماذج الأولية لكائن آخر.
console.log(Dog.prototype.isPrototypeOf(myDog)); // Output: true console.log(Animal.prototype.isPrototypeOf(myDog)); // Output: true console.log(Object.prototype.isPrototypeOf(myDog)); // Output: true
خصائص التظليل (Shadowing Properties)
يقال إن الخاصية الموجودة في كائن ما تظلل (shadow) خاصية في نموذجها الأولي إذا كان لها نفس الاسم. عند الوصول إلى الخاصية، يتم استرداد تلك الموجودة في الكائن نفسه، ويتم تجاهل تلك الموجودة في النموذج الأولي (حتى يتم حذف خاصية الكائن). ينطبق هذا على كل من خصائص البيانات والأساليب.
class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello from Person: ${this.name}`);
}
}
class Employee extends Person {
constructor(name, id) {
super(name);
this.id = id;
}
// تظليل طريقة greet من Person
greet() {
console.log(`Hello from Employee: ${this.name}, ID: ${this.id}`);
}
}
const emp = new Employee("Jane", "E123");
emp.greet(); // Output: Hello from Employee: Jane, ID: E123
// لاستدعاء طريقة greet الخاصة بالأب، سنحتاج إلى super.greet()
الخاتمة
سلسلة النماذج الأولية في جافاسكريبت هي مفهوم أساسي يدعم كيفية إنشاء الكائنات، وكيفية الوصول إلى الخصائص، وكيفية تحقيق الوراثة. بينما تبسط الصيغ الحديثة مثل فئات ES6 استخدامها، فإن الفهم العميق للنماذج الأولية ضروري لأي مطور جافاسكريبت جاد. من خلال إتقان هذا المفهوم، تكتسب القدرة على كتابة كود أكثر كفاءة وقابلية لإعادة الاستخدام والصيانة، وهو أمر بالغ الأهمية للتعاون الفعال في المشاريع العالمية. سواء كنت تطور لشركة متعددة الجنسيات أو لشركة ناشئة صغيرة ذات قاعدة مستخدمين دولية، فإن الفهم القوي للوراثة القائمة على النموذج الأولي في جافاسكريبت سيكون بمثابة أداة قوية في ترسانة التطوير الخاصة بك.
استمر في الاستكشاف، استمر في التعلم، وبرمجة سعيدة!