أساسيات JavaScript

النماذج الأولية وسلسلة النماذج الأولية

50 دقيقة الدرس 35 من 60

فهم النماذج الأولية في JavaScript

JavaScript هي لغة قائمة على النماذج الأولية (Prototypes). على عكس لغات البرمجة الكائنية التقليدية مثل Java أو ++C التي تعتمد على الأصناف (Classes) كمخططات، تستخدم JavaScript النماذج الأولية كآلية للوراثة والسلوك المشترك. كل كائن في JavaScript يحتوي على رابط داخلي لكائن آخر يسمى نموذجه الأولي. عندما تصل إلى خاصية على كائن وتلك الخاصية غير موجودة على الكائن نفسه، تتبع JavaScript رابط النموذج الأولي للبحث عن تلك الخاصية في كائن النموذج الأولي، ثم في النموذج الأولي للنموذج الأولي، وهكذا حتى تصل إلى نهاية السلسلة. هذه الآلية تعرف بسلسلة النماذج الأولية، وهي واحدة من أكثر المفاهيم أساسية في JavaScript.

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

خاصية __proto__ مقابل خاصية prototype

أحد أكثر الجوانب إرباكا في نماذج JavaScript الأولية هو التمييز بين __proto__ و prototype. هاتان الخاصيتان تخدمان أغراضا مختلفة، والخلط بينهما مصدر شائع للأخطاء.

خاصية __proto__ موجودة على كل كائن. إنها المرجع الفعلي لنموذج الكائن الأولي -- الكائن الذي يرث منه الخصائص والدوال. عندما يبحث محرك JavaScript عن خاصية على كائن ولا يجدها، يتبع رابط __proto__ لمواصلة البحث.

خاصية prototype موجودة فقط على الدوال (تحديدا، دوال البناء والأصناف). إنها الكائن الذي سيصبح __proto__ لأي كائن جديد يتم إنشاؤه باستخدام تلك الدالة كمنشئ مع الكلمة المفتاحية new.

مثال: __proto__ مقابل prototype

function Person(name) {
    this.name = name;
}

Person.prototype.greet = function() {
    return 'Hello, I am ' + this.name;
};

const alice = new Person('Alice');

// alice.__proto__ يشير إلى Person.prototype
console.log(alice.__proto__ === Person.prototype); // true

// Person.prototype هو كائن يحتوي على دالة greet
console.log(alice.greet()); // "Hello, I am Alice"

// alice نفسه لا يملك greet -- إنها تأتي من النموذج الأولي
console.log(alice.hasOwnProperty('greet')); // false
console.log(alice.hasOwnProperty('name'));  // true
ملاحظة: خاصية __proto__ هي ميزة قديمة. بينما هي مدعومة على نطاق واسع من قبل المتصفحات للتوافق، إلا أنها ليست جزءا من مواصفات ECMAScript الرسمية كخاصية قياسية. الطريقة الموصى بها للحصول على أو تعيين النموذج الأولي لكائن هي من خلال Object.getPrototypeOf() و Object.setPrototypeOf().

Object.getPrototypeOf() و Object.setPrototypeOf()

الطريقة الحديثة والموصى بها للتعامل مع النموذج الأولي لكائن هي من خلال الدوال الثابتة Object.getPrototypeOf() و Object.setPrototypeOf(). توفر هذه الدوال واجهة نظيفة وقياسية لقراءة وتعديل سلسلة النماذج الأولية.

مثال: استخدام Object.getPrototypeOf()

function Animal(type) {
    this.type = type;
}

Animal.prototype.describe = function() {
    return 'I am a ' + this.type;
};

const dog = new Animal('Dog');

// الحصول على النموذج الأولي لـ dog
const proto = Object.getPrototypeOf(dog);
console.log(proto === Animal.prototype); // true
console.log(proto.describe);             // [Function: describe]

// التحقق من سلسلة النماذج الأولية أبعد
const protoOfProto = Object.getPrototypeOf(proto);
console.log(protoOfProto === Object.prototype); // true

// نهاية السلسلة
console.log(Object.getPrototypeOf(protoOfProto)); // null
تحذير: تجنب استخدام Object.setPrototypeOf() في الكود الحساس للأداء. تغيير النموذج الأولي لكائن موجود يجبر محرك JavaScript على التخلي عن التحسينات التي أجراها لشكل ذلك الكائن، مما يؤدي إلى تدهور كبير في الأداء. إذا كنت بحاجة لتعيين نموذج أولي، فضل القيام بذلك عند وقت إنشاء الكائن باستخدام Object.create().

Object.create() -- إنشاء كائنات بنموذج أولي محدد

تنشئ دالة Object.create() كائنا جديدا بنموذج أولي محدد. هذه هي الطريقة الأنظف والأكثر كفاءة لإنشاء علاقات النماذج الأولية. تتيح لك إنشاء كائنات ترث من أي كائن آخر دون الحاجة لدالة بناء.

مثال: أساسيات Object.create()

const vehicle = {
    start: function() {
        return this.make + ' is starting...';
    },
    stop: function() {
        return this.make + ' has stopped.';
    }
};

// إنشاء كائن جديد مع vehicle كنموذجه الأولي
const car = Object.create(vehicle);
car.make = 'Toyota';
car.doors = 4;

console.log(car.start()); // "Toyota is starting..."
console.log(car.stop());  // "Toyota has stopped."

// car لا يملك start أو stop -- إنها تأتي من النموذج الأولي
console.log(car.hasOwnProperty('start')); // false
console.log(car.hasOwnProperty('make'));  // true

// التحقق من رابط النموذج الأولي
console.log(Object.getPrototypeOf(car) === vehicle); // true

مثال: Object.create() مع واصفات الخصائص

const base = {
    type: 'base',
    identify: function() {
        return 'Type: ' + this.type;
    }
};

const derived = Object.create(base, {
    type: {
        value: 'derived',
        writable: true,
        enumerable: true,
        configurable: true
    },
    version: {
        value: 2,
        writable: false,
        enumerable: true,
        configurable: false
    }
});

console.log(derived.identify()); // "Type: derived"
console.log(derived.version);    // 2
derived.version = 3;             // يفشل بصمت (أو يرمي خطأ في الوضع الصارم)
console.log(derived.version);    // 2

البحث في سلسلة النماذج الأولية -- كيف تجد JavaScript الخصائص

عندما تصل إلى خاصية على كائن، تتبع JavaScript خوارزمية دقيقة. تبحث أولا في الكائن نفسه عن الخاصية. إذا لم يتم العثور على الخاصية، تتبع رابط __proto__ إلى النموذج الأولي للكائن وتبحث هناك. تتكرر هذه العملية، وتنتقل أعلى في السلسلة من نموذج أولي إلى آخر، حتى يتم العثور على الخاصية أو تنتهي السلسلة عند null. إذا لم يتم العثور على الخاصية في أي مكان في السلسلة، يتم إرجاع undefined.

مثال: البحث في سلسلة النماذج الأولية عمليا

const grandparent = {
    family: 'Smith',
    heritage: function() {
        return 'The ' + this.family + ' family';
    }
};

const parent = Object.create(grandparent);
parent.job = 'Engineer';

const child = Object.create(parent);
child.name = 'Alice';

// سلسلة البحث عن الخصائص:
// 1. child.name -- تم العثور عليها على child نفسه
console.log(child.name); // "Alice"

// 2. child.job -- ليست على child، تم العثور عليها على parent
console.log(child.job); // "Engineer"

// 3. child.family -- ليست على child، ليست على parent،
//    تم العثور عليها على grandparent
console.log(child.family); // "Smith"

// 4. child.heritage() -- الدالة موجودة على grandparent
console.log(child.heritage()); // "The Smith family"

// 5. child.age -- لم يتم العثور عليها في أي مكان في السلسلة
console.log(child.age); // undefined

تظليل الخصائص (Property Shadowing)

يحدث تظليل الخصائص عندما يمتلك كائن خاصية بنفس الاسم كخاصية على نموذجه الأولي. خاصية الكائن الخاصة "تظلل" أو تخفي خاصية النموذج الأولي. عندما تصل إلى تلك الخاصية، تحصل على نسخة الكائن الخاصة، وليس نسخة النموذج الأولي. خاصية النموذج الأولي لا تزال موجودة ويمكن الوصول إليها من خلال كائنات أخرى تشترك في ذلك النموذج الأولي.

مثال: تظليل الخصائص

const defaults = {
    color: 'blue',
    size: 'medium',
    getInfo: function() {
        return this.color + ' / ' + this.size;
    }
};

const custom = Object.create(defaults);
custom.color = 'red'; // يظلل defaults.color

console.log(custom.color);     // "red" (خاصية خاصة)
console.log(defaults.color);   // "blue" (لم تتغير)
console.log(custom.size);      // "medium" (موروثة)
console.log(custom.getInfo()); // "red / medium"

// حذف الخاصية الخاصة يكشف خاصية النموذج الأولي
delete custom.color;
console.log(custom.color);     // "blue" (موروثة مرة أخرى)
console.log(custom.getInfo()); // "blue / medium"
نصيحة احترافية: تظليل الخصائص هو الآلية التي تقف وراء تجاوز الدوال (Method Overriding) في JavaScript. عندما يعرف صنف فرعي دالة بنفس اسم دالة على نموذج الصنف الأب الأولي، نسخة الصنف الفرعي تظلل نسخة الصنف الأب. يمكنك الوصول إلى الدالة الأصلية باستخدام Object.getPrototypeOf(obj).method.call(obj) أو من خلال super.method() في بناء جملة الأصناف.

خاصية constructor

كل كائن prototype لدالة يحتوي على خاصية constructor تشير مرة أخرى إلى الدالة نفسها. هذا ينشئ مرجعا دائريا: الدالة تشير إلى نموذجها الأولي، والنموذج الأولي يشير مرة أخرى إلى الدالة. تستخدم هذه الخاصية لتحديد أي دالة بناء أنشأت كائنا ما.

مثال: خاصية constructor

function Car(make, model) {
    this.make = make;
    this.model = model;
}

const myCar = new Car('Honda', 'Civic');

// constructor يشير مرة أخرى إلى دالة Car
console.log(myCar.constructor === Car); // true

// يمكنك استخدام constructor لإنشاء نسخ جديدة
const anotherCar = new myCar.constructor('Ford', 'Focus');
console.log(anotherCar.make);  // "Ford"
console.log(anotherCar.model); // "Focus"

// كن حذرا عند استبدال prototype بالكامل
Car.prototype = {
    drive: function() { return 'Driving...'; }
};

const newCar = new Car('BMW', 'X5');
console.log(newCar.constructor === Car);    // false!
console.log(newCar.constructor === Object); // true

// استعد constructor دائما عند استبدال prototype
Car.prototype = {
    constructor: Car,
    drive: function() { return 'Driving...'; }
};

const fixedCar = new Car('Audi', 'A4');
console.log(fixedCar.constructor === Car); // true

أنماط الوراثة بالنماذج الأولية

قبل أصناف ES6، استخدم المطورون عدة أنماط لتنفيذ الوراثة في JavaScript. فهم هذه الأنماط مهم لأنها تكشف كيف تعمل وراثة JavaScript فعليا، وبناء جملة class هو مجرد سكر نحوي فوق هذه الأنماط ذاتها.

مثال: نمط المنشئ مع سلسلة النماذج الأولية

// المنشئ الأب
function Shape(color) {
    this.color = color;
}

Shape.prototype.describe = function() {
    return 'A ' + this.color + ' shape';
};

// المنشئ الابن
function Circle(color, radius) {
    Shape.call(this, color); // استدعاء منشئ الأب
    this.radius = radius;
}

// إعداد الوراثة -- Circle.prototype يرث من Shape.prototype
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle; // استعادة constructor

Circle.prototype.area = function() {
    return Math.PI * this.radius * this.radius;
};

Circle.prototype.describe = function() {
    const parentDesc = Shape.prototype.describe.call(this);
    return parentDesc + ' (circle, radius: ' + this.radius + ')';
};

const c = new Circle('red', 5);
console.log(c.describe());         // "A red shape (circle, radius: 5)"
console.log(c.area().toFixed(2));   // "78.54"
console.log(c instanceof Circle);  // true
console.log(c instanceof Shape);   // true

مثال: الوراثة القائمة على الكائنات البحتة (بدون منشئات)

const eventEmitter = {
    _listeners: null,

    on: function(event, callback) {
        if (!this._listeners) this._listeners = {};
        if (!this._listeners[event]) this._listeners[event] = [];
        this._listeners[event].push(callback);
    },

    emit: function(event, data) {
        if (!this._listeners || !this._listeners[event]) return;
        this._listeners[event].forEach(function(cb) {
            cb(data);
        });
    }
};

const logger = Object.create(eventEmitter);
logger.log = function(message) {
    console.log('[LOG] ' + message);
    this.emit('logged', { message: message });
};

logger.on('logged', function(data) {
    console.log('Event fired: ' + data.message);
});

logger.log('Application started');
// [LOG] Application started
// Event fired: Application started

دوال Object.prototype

Object.prototype يقع في أعلى سلسلة النماذج الأولية لكل كائن تقريبا في JavaScript. يوفر عدة دوال متاحة لجميع الكائنات. فهم هذه الدوال مهم لأنها غالبا ما يتم تجاوزها لتخصيص سلوك الكائن.

hasOwnProperty()

دالة hasOwnProperty() تعيد true إذا كان الكائن يمتلك الخاصية المحددة كخاصية خاصة (غير موروثة). هذا ضروري للتمييز بين الخصائص الخاصة والموروثة.

مثال: hasOwnProperty() للتحقق الآمن من الخصائص

const config = Object.create({ defaultTimeout: 3000 });
config.apiUrl = 'https://api.example.com';
config.retries = 3;

// التكرار فقط على الخصائص الخاصة
for (const key in config) {
    if (config.hasOwnProperty(key)) {
        console.log(key + ': ' + config[key]);
    }
}
// المخرجات:
// apiUrl: https://api.example.com
// retries: 3
// (defaultTimeout تم تخطيها لأنها موروثة)

// البديل الحديث: Object.hasOwn() (ES2022)
for (const key in config) {
    if (Object.hasOwn(config, key)) {
        console.log(key + ': ' + config[key]);
    }
}

toString() و valueOf()

دالة toString() تعيد تمثيلا نصيا للكائن، وvalueOf() تعيد القيمة البدائية للكائن. يتم استدعاء هذه الدوال تلقائيا بواسطة JavaScript في سياقات معينة، مثل ربط السلاسل النصية أو العمليات الحسابية.

مثال: تجاوز toString() و valueOf()

function Money(amount, currency) {
    this.amount = amount;
    this.currency = currency;
}

Money.prototype.toString = function() {
    return this.amount.toFixed(2) + ' ' + this.currency;
};

Money.prototype.valueOf = function() {
    return this.amount;
};

const price = new Money(29.99, 'USD');

// toString() يتم استدعاؤها في سياق نصي
console.log('Price: ' + price);        // "Price: 29.99 USD"
console.log(String(price));             // "29.99 USD"
console.log(`Item costs ${price}`);     // "Item costs 29.99 USD"

// valueOf() يتم استدعاؤها في سياق رقمي
console.log(price + 10);    // 39.99
console.log(price > 20);    // true
console.log(price * 2);     // 59.98

توسيع النماذج الأولية المدمجة (ولماذا لا يجب ذلك)

لأن JavaScript تسمح لك بتعديل أي نموذج أولي، يمكنك إضافة دوال إلى كائنات مدمجة مثل Array.prototype أو String.prototype أو حتى Object.prototype. بينما هذا ممكن تقنيا، إلا أنه يعتبر ممارسة سيئة جدا لعدة أسباب مهمة.

مثال: توسيع النماذج الأولية المدمجة (خطير!)

// إضافة دالة إلى Array.prototype -- لا تفعل هذا
Array.prototype.last = function() {
    return this[this.length - 1];
};

const numbers = [1, 2, 3, 4, 5];
console.log(numbers.last()); // 5

// لماذا هذا خطير:

// 1. تصادم الأسماء -- قد يضيف معيار JavaScript مستقبلي
//    Array.prototype.last بسلوك مختلف
// 2. يكسر حلقات for...in على المصفوفات
const arr = [10, 20, 30];
for (const key in arr) {
    console.log(key); // "0", "1", "2", "last" -- غير متوقع!
}

// 3. يؤثر على جميع المصفوفات في التطبيق بأكمله
// 4. المكتبات الخارجية قد تضيف نفس الدالة بشكل مختلف
// 5. يجعل تصحيح الأخطاء صعبا للغاية

// التنظيف
delete Array.prototype.last;
تحذير: لا تعدل أبدا النماذج الأولية المدمجة في كود الإنتاج. إذا كنت بحاجة لدوال مساعدة مخصصة، أنشئ دوالا مستقلة أو استخدم صنف مساعد بدلا من ذلك. الاستثناء المقبول الوحيد هو الـ polyfilling -- إضافة دوال قياسية مفقودة في بيئات أقدم، وحتى في هذه الحالة يجب التحقق مما إذا كانت الدالة موجودة بالفعل قبل إضافتها.

مثال: نمط Polyfill الآمن

// polyfill آمن -- أضف فقط إذا لم تكن الدالة موجودة
if (!Array.prototype.at) {
    Array.prototype.at = function(index) {
        if (index < 0) {
            return this[this.length + index];
        }
        return this[index];
    };
}

// بديل أفضل: دوال مساعدة مستقلة
function lastElement(arr) {
    return arr[arr.length - 1];
}

console.log(lastElement([1, 2, 3])); // 3

وراثة الأصناف تحت الغطاء

قدم ES6 بناء جملة class، الذي يوفر طريقة أنظف لتعريف المنشئات وإعداد سلاسل النماذج الأولية. ومع ذلك، الأصناف في JavaScript ليست نموذج وراثة جديد -- إنها سكر نحوي فوق نظام الوراثة بالنماذج الأولية الموجود. فهم ما يحدث تحت الغطاء يساعدك في تصحيح المشاكل وكتابة كود أكثر فعالية.

مثال: بناء جملة الأصناف مقابل المكافئ بالنماذج الأولية

// بناء جملة أصناف ES6
class Animal {
    constructor(name, sound) {
        this.name = name;
        this.sound = sound;
    }

    speak() {
        return this.name + ' says ' + this.sound;
    }

    static create(name, sound) {
        return new Animal(name, sound);
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name, 'Woof');
        this.tricks = [];
    }

    learn(trick) {
        this.tricks.push(trick);
    }

    showTricks() {
        return this.name + ' knows: ' + this.tricks.join(', ');
    }
}

// ما يفعله المحرك فعليا (مبسط):
// function Animal(name, sound) {
//     this.name = name;
//     this.sound = sound;
// }
// Animal.prototype.speak = function() { ... };
// Animal.create = function(name, sound) { ... };
//
// function Dog(name) {
//     Animal.call(this, name, 'Woof');
//     this.tricks = [];
// }
// Dog.prototype = Object.create(Animal.prototype);
// Dog.prototype.constructor = Dog;
// Object.setPrototypeOf(Dog, Animal);

const rex = new Dog('Rex');
rex.learn('sit');
rex.learn('shake');

console.log(rex.speak());      // "Rex says Woof"
console.log(rex.showTricks()); // "Rex knows: sit, shake"

// إثبات أن كل شيء نماذج أولية تحت الغطاء
console.log(typeof Animal);                          // "function"
console.log(rex.__proto__ === Dog.prototype);        // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(rex instanceof Dog);    // true
console.log(rex instanceof Animal); // true

تصور سلسلة النماذج الأولية

فهم سلسلة النماذج الأولية يصبح أسهل بكثير عندما تتصورها كسلسلة مترابطة من الكائنات. كل سلسلة تنتهي في النهاية عند Object.prototype، الذي نموذجه الأولي هو null.

مثال: تتبع سلسلة النماذج الأولية الكاملة

function tracePrototypeChain(obj) {
    const chain = [];
    let current = obj;

    while (current !== null) {
        if (current === obj) {
            chain.push('[instance: ' + (current.constructor?.name || 'Object') + ']');
        } else if (current.constructor) {
            chain.push(current.constructor.name + '.prototype');
        } else {
            chain.push('[null prototype]');
        }
        current = Object.getPrototypeOf(current);
    }
    chain.push('null');
    return chain.join(' --> ');
}

// كائن عادي
const obj = { x: 1 };
console.log(tracePrototypeChain(obj));
// [instance: Object] --> Object.prototype --> null

// مصفوفة
const arr = [1, 2, 3];
console.log(tracePrototypeChain(arr));
// [instance: Array] --> Array.prototype --> Object.prototype --> null

// سلسلة وراثة مخصصة
function Base() {}
function Middle() {}
Middle.prototype = Object.create(Base.prototype);
Middle.prototype.constructor = Middle;
function Leaf() {}
Leaf.prototype = Object.create(Middle.prototype);
Leaf.prototype.constructor = Leaf;

const instance = new Leaf();
console.log(tracePrototypeChain(instance));
// [instance: Leaf] --> Leaf.prototype --> Middle.prototype --> Base.prototype --> Object.prototype --> null

// كائن بنموذج أولي null (بدون سلسلة)
const bare = Object.create(null);
console.log(Object.getPrototypeOf(bare)); // null
// bare ليس لديه أي دوال موروثة
ملاحظة: الكائنات المنشأة بواسطة Object.create(null) ليس لديها سلسلة نماذج أولية على الإطلاق. تسمى أحيانا "كائنات القاموس" أو "الكائنات العارية" لأنها تحتوي على صفر خصائص موروثة. هذا يجعلها مفيدة كمخازن مفتاح-قيمة نقية حيث لا تريد للخصائص الموروثة مثل toString أو hasOwnProperty أن تتداخل مع بياناتك.

تأثيرات الأداء للنماذج الأولية

سلسلة النماذج الأولية لها تأثيرات مباشرة على الأداء. فهم هذه التأثيرات يساعدك في كتابة كود أسرع وتجنب مشاكل الأداء الشائعة.

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

كفاءة الذاكرة: الدوال المعرفة على النموذج الأولي مشتركة عبر جميع النسخ. هذا أكثر كفاءة في الذاكرة بكثير من تعريف الدوال داخل المنشئ، الذي ينشئ كائن دالة جديد لكل نسخة.

مثال: كفاءة الذاكرة -- النموذج الأولي مقابل دوال المنشئ

// سيء: الدوال في المنشئ (دالة جديدة لكل نسخة)
function IneffcientUser(name) {
    this.name = name;
    this.greet = function() { // دالة جديدة تنشأ لكل نسخة
        return 'Hello, ' + this.name;
    };
}

// جيد: الدوال على النموذج الأولي (مشتركة عبر جميع النسخ)
function EfficientUser(name) {
    this.name = name;
}
EfficientUser.prototype.greet = function() { // دالة واحدة مشتركة للجميع
    return 'Hello, ' + this.name;
};

const users1 = [];
const users2 = [];

for (let i = 0; i < 1000; i++) {
    users1.push(new IneffcientUser('User' + i));
    users2.push(new EfficientUser('User' + i));
}

// users1: 1000 دالة greet منفصلة في الذاكرة
// users2: دالة greet واحدة مشتركة على النموذج الأولي

// التحقق من أنها كائنات دوال مختلفة (غير فعال)
console.log(users1[0].greet === users1[1].greet); // false

// التحقق من أنها تشارك نفس كائن الدالة (فعال)
console.log(users2[0].greet === users2[1].greet); // true

مثال: تأثير الأداء لعمق سلسلة النماذج الأولية

// سلسلة ضحلة -- بحث سريع
const shallow = Object.create({ sharedMethod: function() { return 42; } });

// سلسلة عميقة -- بحث أبطأ للخصائص الموروثة بعمق
let deep = { deepMethod: function() { return 42; } };
for (let i = 0; i < 20; i++) {
    deep = Object.create(deep);
}

// قياس الأداء: الوصول إلى دالة على سلسلة ضحلة مقابل عميقة
console.time('shallow');
for (let i = 0; i < 1000000; i++) {
    shallow.sharedMethod();
}
console.timeEnd('shallow');

console.time('deep');
for (let i = 0; i < 1000000; i++) {
    deep.deepMethod();
}
console.timeEnd('deep');

// نصيحة: خزن الخصائص الموروثة بعمق في متغيرات محلية
const cachedMethod = deep.deepMethod;
console.time('cached');
for (let i = 0; i < 1000000; i++) {
    cachedMethod();
}
console.timeEnd('cached');
نصيحة احترافية: حافظ على سلاسل النماذج الأولية ضحلة. عمليا، عمق سلسلة من مستويين إلى ثلاثة شائع ويعمل جيدا. السلاسل العميقة للغاية (أكثر من خمسة إلى ستة مستويات) يمكن أن تؤثر على أداء البحث عن الخصائص، خاصة في مسارات الكود الساخنة. إذا كنت بحاجة لتسلسلات وراثة متداخلة بعمق، فكر في التركيب (Composition) بدلا من الوراثة كنمط بديل.

أنماط عملية وأفضل الممارسات

الآن بعد أن فهمت آليات النماذج الأولية، إليك بعض الأنماط العملية وأفضل الممارسات لاستخدامها بفعالية في التطبيقات الحقيقية.

مثال: نمط Mixin باستخدام Object.assign()

// المزيجات تسمح لك بتركيب سلوك من مصادر متعددة
const serializable = {
    toJSON: function() {
        const result = {};
        for (const key in this) {
            if (this.hasOwnProperty(key) && typeof this[key] !== 'function') {
                result[key] = this[key];
            }
        }
        return JSON.stringify(result);
    }
};

const validatable = {
    validate: function() {
        for (const rule of this._validationRules || []) {
            if (!rule.check(this[rule.field])) {
                return { valid: false, error: rule.message };
            }
        }
        return { valid: true };
    }
};

function Product(name, price) {
    this.name = name;
    this.price = price;
    this._validationRules = [
        { field: 'name', check: function(v) { return v && v.length > 0; }, message: 'Name is required' },
        { field: 'price', check: function(v) { return v > 0; }, message: 'Price must be positive' }
    ];
}

// مزج القدرات
Object.assign(Product.prototype, serializable, validatable);

const item = new Product('Widget', 9.99);
console.log(item.toJSON());    // '{"name":"Widget","price":9.99}'
console.log(item.validate());  // { valid: true }

مثال: التحقق من علاقات النماذج الأولية

function Shape() {}
function Rect() {}
Rect.prototype = Object.create(Shape.prototype);
Rect.prototype.constructor = Rect;

const r = new Rect();

// instanceof يتحقق من سلسلة النماذج الأولية بالكامل
console.log(r instanceof Rect);   // true
console.log(r instanceof Shape);  // true
console.log(r instanceof Object); // true

// isPrototypeOf يتحقق مما إذا كان كائن موجودا في سلسلة كائن آخر
console.log(Shape.prototype.isPrototypeOf(r)); // true
console.log(Rect.prototype.isPrototypeOf(r));  // true
console.log(Array.prototype.isPrototypeOf(r)); // false

تمرين عملي

أنشئ تسلسلا وراثيا لنظام إدارة مكتبة. عرف منشئا أساسيا LibraryItem يأخذ title و year كمعاملات ويخزنهما كخصائص نسخة. أضف دالة getSummary() إلى LibraryItem.prototype تعيد سلسلة نصية بالتنسيق "Title (Year)". ثم أنشئ منشئ Book يمتد من LibraryItem ويضيف خاصية author وخاصية pageCount. تجاوز getSummary() على Book.prototype لتتضمن اسم المؤلف. أنشئ منشئ DVD يمتد من LibraryItem ويضيف خاصية director وخاصية duration. تجاوز getSummary() على DVD.prototype لتتضمن المخرج والمدة. اكتب دالة traceChain() تأخذ أي كائن وتعيد مصفوفة من أسماء المنشئات في سلسلة نماذجه الأولية. اختبر مع نسخ من كل من Book و DVD. تحقق من أن instanceof يعمل بشكل صحيح لجميع المنشئات الثلاثة. أخيرا، أنشئ مزيجا (mixin) يسمى borrowable يضيف دالتي checkOut() و checkIn()، وامزجه في كل من Book.prototype و DVD.prototype.