النماذج الأولية وسلسلة النموذج الأولي
فهم النماذج الأولية أمر بالغ الأهمية لإتقان JavaScript. كل كائن JavaScript له نموذج أولي، وهو آلية للوراثة. دعنا نتعمق في كيفية عمل النماذج الأولية وكيف تدعم نموذج الوراثة في JavaScript.
ما هو النموذج الأولي؟
النموذج الأولي هو كائن ترث منه كائنات أخرى الخصائص والدوال. في JavaScript، كل كائن لديه رابط داخلي إلى كائن آخر يُسمى نموذجه الأولي.
مفهوم أساسي: تستخدم JavaScript الوراثة النموذجية، وليس الوراثة الكلاسيكية مثل Java أو C++. فئات ES6 هي سكر نحوي فوق هذا النظام القائم على النموذج الأولي.
فهم __proto__ مقابل prototype
هناك خاصيتان مهمتان يجب فهمهما:
// كل كائن لديه __proto__ (رابط إلى نموذجه الأولي)
const obj = {};
console.log(obj.__proto__); // Object.prototype
// دوال المُنشئ لديها خاصية prototype
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const john = new Person("John");
// __proto__ يشير إلى prototype المُنشئ
console.log(john.__proto__ === Person.prototype); // true
// النموذج الأولي لديه خاصية constructor تشير إلى الوراء
console.log(Person.prototype.constructor === Person); // true
الاختلافات الرئيسية:
__proto__:
- خاصية على كل كائن
- تشير إلى النموذج الأولي للكائن
- تُستخدم للبحث في سلسلة النموذج الأولي
prototype:
- خاصية على دوال المُنشئ والفئات
- تُعرّف الخصائص/الدوال للنسخ
- تُستخدم عند إنشاء كائنات جديدة باستخدام "new"
مهم: بينما يعمل __proto__ في المتصفحات، فهو مُهمل. استخدم Object.getPrototypeOf() و Object.setPrototypeOf() بدلاً من ذلك للكود الإنتاجي.
سلسلة النموذج الأولي
عند الوصول إلى خاصية على كائن، تبحث JavaScript أولاً عنها على الكائن نفسه. إذا لم تُجد، تبحث في نموذج الكائن الأولي، ثم في نموذج النموذج الأولي، وهكذا:
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name} is eating`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// إعداد الوراثة
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
return `${this.name} says Woof!`;
};
const myDog = new Dog("Max", "Labrador");
console.log(myDog.bark()); // "Max says Woof!" (وُجد في Dog.prototype)
console.log(myDog.eat()); // "Max is eating" (وُجد في Animal.prototype)
console.log(myDog.toString()); // "[object Object]" (وُجد في Object.prototype)
// تصوير سلسلة النموذج الأولي:
// myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null
التحقق من سلسلة النموذج الأولي
عدة دوال تساعدك في فحص سلسلة النموذج الأولي:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const john = new Person("John");
// احصل على النموذج الأولي
console.log(Object.getPrototypeOf(john) === Person.prototype); // true
// تحقق مما إذا كانت الخاصية موجودة على الكائن (وليس النموذج الأولي)
console.log(john.hasOwnProperty("name")); // true
console.log(john.hasOwnProperty("greet")); // false (إنها في النموذج الأولي)
// تحقق مما إذا كانت الخاصية موجودة في أي مكان في السلسلة
console.log("name" in john); // true
console.log("greet" in john); // true
console.log("toString" in john); // true (من Object.prototype)
// تحقق مما إذا كان الكائن في سلسلة النموذج الأولي
console.log(Person.prototype.isPrototypeOf(john)); // true
console.log(Object.prototype.isPrototypeOf(john)); // true
console.log(Array.prototype.isPrototypeOf(john)); // false
Object.create() للوراثة النموذجية
Object.create() ينشئ كائناً جديداً مع نموذج أولي محدد:
// إنشاء كائن نموذج أولي
const personPrototype = {
greet() {
return `Hello, I'm ${this.name}`;
},
introduce() {
return `My name is ${this.name} and I'm ${this.age} years old`;
}
};
// إنشاء كائنات مع هذا النموذج الأولي
const john = Object.create(personPrototype);
john.name = "John";
john.age = 30;
const sarah = Object.create(personPrototype);
sarah.name = "Sarah";
sarah.age = 25;
console.log(john.greet()); // "Hello, I'm John"
console.log(sarah.introduce()); // "My name is Sarah and I'm 25 years old"
// كلاهما يشترك في نفس النموذج الأولي
console.log(Object.getPrototypeOf(john) === Object.getPrototypeOf(sarah)); // true
سلسلة النموذج الأولي مع الفئات
فئات ES6 تستخدم أيضاً سلسلة النموذج الأولي تحت الغطاء:
class Animal {
constructor(name) {
this.name = name;
}
eat() {
return `${this.name} is eating`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
return `${this.name} says Woof!`;
}
}
const myDog = new Dog("Max", "Labrador");
// تحقق من سلسلة النموذج الأولي
console.log(myDog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true (نهاية السلسلة)
// سلسلة النموذج الأولي:
// myDog --> Dog.prototype --> Animal.prototype --> Object.prototype --> null
تعديل النماذج الأولية
يمكنك إضافة أو تعديل دوال النموذج الأولي حتى بعد إنشاء الكائنات:
function Calculator() {
this.result = 0;
}
Calculator.prototype.add = function(num) {
this.result += num;
return this;
};
const calc1 = new Calculator();
const calc2 = new Calculator();
calc1.add(5).add(3);
console.log(calc1.result); // 8
// إضافة دالة جديدة إلى النموذج الأولي
Calculator.prototype.multiply = function(num) {
this.result *= num;
return this;
};
// كل من النسخ الموجودة والجديدة تحصل على الدالة
calc1.multiply(2);
console.log(calc1.result); // 16
calc2.add(10).multiply(3);
console.log(calc2.result); // 30
أفضل ممارسة: بينما يمكنك تعديل النماذج الأولية في وقت التشغيل، فإنه غير موصى به عموماً لأنه قد يؤدي إلى سلوك غير متوقع ومشاكل في الأداء. عرّف جميع الدوال مقدماً.
النماذج الأولية المدمجة
كائنات JavaScript المدمجة تستخدم أيضاً النماذج الأولية:
// نموذج أولي للمصفوفة
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
// دوال المصفوفة تأتي من النموذج الأولي
console.log(arr.hasOwnProperty("push")); // false
console.log("push" in arr); // true
console.log(Array.prototype.hasOwnProperty("push")); // true
// نموذج أولي للسلسلة النصية
const str = "hello";
console.log(str.__proto__ === String.prototype); // true
// نموذج أولي للرقم
const num = 42;
console.log(num.__proto__ === Number.prototype); // true
// نموذج أولي للدالة (نعم، الدوال هي كائنات!)
function myFunc() {}
console.log(myFunc.__proto__ === Function.prototype); // true
توسيع النماذج الأولية المدمجة (كن حذراً!)
يمكنك توسيع النماذج الأولية المدمجة، لكن هذا غير مستحسن عموماً:
// إضافة دالة إلى نموذج أولي للمصفوفة
Array.prototype.last = function() {
return this[this.length - 1];
};
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.last()); // 5
// إضافة إلى نموذج أولي للسلسلة النصية
String.prototype.reverse = function() {
return this.split("").reverse().join("");
};
console.log("hello".reverse()); // "olleh"
تحذير: توسيع النماذج الأولية المدمجة يمكن أن يسبب تعارضات مع ميزات JavaScript المستقبلية أو مكتبات الطرف الثالث. افعل هذا فقط إذا كان ضرورياً تماماً وتأكد من أسماء دوال فريدة.
حجب الخصائص
عند تعيين خاصية على كائن، فإنها تحجب خاصية النموذج الأولي:
const parent = {
name: "Parent",
greet() {
return `Hello from ${this.name}`;
}
};
const child = Object.create(parent);
console.log(child.name); // "Parent" (من النموذج الأولي)
console.log(child.greet()); // "Hello from Parent"
// حجب خاصية name
child.name = "Child";
console.log(child.name); // "Child" (خاصية خاصة)
console.log(child.greet()); // "Hello from Child" (يستخدم الخاصية الخاصة)
// تحقق من الخصائص
console.log(child.hasOwnProperty("name")); // true
console.log(child.hasOwnProperty("greet")); // false
// احذف الخاصية الخاصة للكشف عن خاصية النموذج الأولي
delete child.name;
console.log(child.name); // "Parent" (من النموذج الأولي مرة أخرى)
البدائل الحديثة للنماذج الأولية
توفر JavaScript الحديثة بدائل أنظف لمعظم حالات الاستخدام:
// الطريقة القديمة: المُنشئ + النموذج الأولي
function PersonOld(name, age) {
this.name = name;
this.age = age;
}
PersonOld.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
// الطريقة الحديثة: فئة ES6
class PersonNew {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// الطريقة الحديثة: كائن مع دوال
const createPerson = (name, age) => ({
name,
age,
greet() {
return `Hello, I'm ${this.name}`;
}
});
// جميع الطرق الثلاث تعمل، لكن الفئات مفضلة
const person1 = new PersonOld("John", 30);
const person2 = new PersonNew("Jane", 25);
const person3 = createPerson("Bob", 35);
اعتبارات الأداء
فهم النماذج الأولية يساعد في تحسين الأداء:
// جيد: الدوال على النموذج الأولي (مُشاركة بواسطة جميع النسخ)
class GoodPerson {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
const p1 = new GoodPerson("John");
const p2 = new GoodPerson("Jane");
// كلاهما يشترك في نفس دالة greet
// سيء: الدوال المُعرَّفة في المُنشئ (نسخة منفصلة لكل نسخة)
class BadPerson {
constructor(name) {
this.name = name;
this.greet = function() {
return `Hello, I'm ${this.name}`;
};
}
}
const p3 = new BadPerson("John");
const p4 = new BadPerson("Jane");
// كل منهما لديه دالة greet خاصة به (يهدر الذاكرة)
console.log(p1.greet === p2.greet); // true (نفس المرجع)
console.log(p3.greet === p4.greet); // false (مراجع مختلفة)
نصيحة أداء: الدوال المُعرَّفة على النموذج الأولي تُشارك بواسطة جميع النسخ، مما يوفر الذاكرة. الدوال المُعرَّفة في المُنشئ تُنشئ نسخة جديدة لكل نسخة.
تمرين تطبيقي:
التحدي: أنشئ تطبيقاً قائماً على النموذج الأولي لقائمة مرتبطة بسيطة بالمتطلبات التالية:
- مُنشئ Node مع خصائص value و next
- مُنشئ LinkedList
- دوال النموذج الأولي: append(value), prepend(value), find(value), size()
- اختبر تطبيقك
الحل:
// مُنشئ Node
function Node(value) {
this.value = value;
this.next = null;
}
// مُنشئ LinkedList
function LinkedList() {
this.head = null;
}
// إضافة دوال إلى النموذج الأولي
LinkedList.prototype.append = function(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
return;
}
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
};
LinkedList.prototype.prepend = function(value) {
const newNode = new Node(value);
newNode.next = this.head;
this.head = newNode;
};
LinkedList.prototype.find = function(value) {
let current = this.head;
while (current) {
if (current.value === value) {
return true;
}
current = current.next;
}
return false;
};
LinkedList.prototype.size = function() {
let count = 0;
let current = this.head;
while (current) {
count++;
current = current.next;
}
return count;
};
LinkedList.prototype.toArray = function() {
const result = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
};
// اختبار التطبيق
const list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.prepend(0);
console.log(list.toArray()); // [0, 1, 2, 3]
console.log(list.size()); // 4
console.log(list.find(2)); // true
console.log(list.find(5)); // false
// تحقق من النموذج الأولي
console.log(list.hasOwnProperty("append")); // false (على النموذج الأولي)
console.log(list.hasOwnProperty("head")); // true (خاصية خاصة)
الملخص
في هذا الدرس، تعلمت:
- النماذج الأولية هي آلية الوراثة في JavaScript
__proto__ يربط الكائنات بنموذجها الأولي
- خاصية
prototype تُعرّف دوال للكائنات المُنشأة بالمُنشئ
- سلسلة النموذج الأولي تسمح بالبحث عن الخصائص عبر مستويات متعددة
Object.create() ينشئ كائنات مع نماذج أولية محددة
- فئات ES6 تستخدم النماذج الأولية تحت الغطاء
- الدوال على النماذج الأولية تُشارك، مما يوفر الذاكرة
- الفئات الحديثة توفر صيغة أنظف من معالجة النموذج الأولي يدوياً
التالي: في الدرس التالي، سنستكشف دوال الكائنات القوية بما في ذلك Object.keys() و Object.values() و Object.entries() والمزيد!