أساسيات JavaScript

فئات ES6 والوراثة

45 دقيقة الدرس 34 من 60

مقدمة في فئات ES6

قبل ES6 (ECMAScript 2015)، استخدم JavaScript دوال البناء والنماذج الأولية لإنشاء الكائنات وتنفيذ الوراثة. رغم قوة هذا النهج، إلا أنه كان مطولا ومربكا للمطورين القادمين من لغات قائمة على الفئات مثل Java و Python و C++. قدم ES6 صيغة class كطريقة أنظف وأكثر بديهية لإنشاء الكائنات والتعامل مع الوراثة. من المهم فهم أن فئات ES6 هي سكر نحوي فوق الوراثة القائمة على النماذج الأولية الموجودة في JavaScript -- فهي لا تقدم نموذجا جديدا للبرمجة كائنية التوجه. تحت الغطاء، لا تزال الفئات تستخدم النماذج الأولية. ومع ذلك، فإن صيغة الفئات تجعل الكود أكثر قابلية للقراءة والتنظيم وأقل عرضة للأخطاء بشكل ملحوظ.

صيغة الفئة والباني

يتم الإعلان عن فئة باستخدام الكلمة المفتاحية class متبوعة باسم. حسب الاتفاقية، تستخدم أسماء الفئات نمط PascalCase (الحرف الأول من كل كلمة كبير). طريقة constructor هي طريقة خاصة تعمل تلقائيا عند إنشاء نسخة جديدة باستخدام الكلمة المفتاحية new. تستخدم لتهيئة خصائص الكائن. يمكن أن تحتوي الفئة على طريقة باني واحدة فقط -- وجود أكثر من واحدة سيطرح SyntaxError.

مثال: إعلان فئة أساسي

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

// إنشاء نسخ باستخدام الكلمة المفتاحية 'new'
const person1 = new Person('Ahmed', 30);
const person2 = new Person('Sara', 25);

console.log(person1.name); // "Ahmed"
console.log(person2.age);  // 25

// typeof يكشف أن الفئات هي دوال تحت الغطاء
console.log(typeof Person); // "function"

// بدون 'new'، تحصل على TypeError
// Person('Ali', 20); // TypeError
ملاحظة: على عكس إعلانات الدوال، إعلانات الفئات لا يتم رفعها (hoisting). يجب عليك الإعلان عن فئة قبل أن تتمكن من استخدامها. محاولة إنشاء نسخة من فئة قبل إعلانها ستطرح ReferenceError. هذا اختيار تصميمي متعمد يشجع على تنظيم أفضل للكود.

طرق الفئة

الطرق المعرفة داخل جسم الفئة تضاف إلى النموذج الأولي للفئة، مما يعني أن جميع النسخ تتشارك نفس مراجع الطرق بدلا من أن يكون لكل نسخة نسختها الخاصة. تعرف الطرق بدون الكلمة المفتاحية function وبدون فواصل بينها. هذه الطرق غير قابلة للعد، مما يعني أنها لن تظهر في حلقات for...in.

مثال: تعريف طرق الفئة

class Calculator {
    constructor(initialValue) {
        this.value = initialValue || 0;
    }

    add(number) {
        this.value += number;
        return this; // يمكن تسلسل الطرق
    }

    subtract(number) {
        this.value -= number;
        return this;
    }

    multiply(number) {
        this.value *= number;
        return this;
    }

    divide(number) {
        if (number === 0) {
            throw new Error('لا يمكن القسمة على صفر');
        }
        this.value /= number;
        return this;
    }

    reset() {
        this.value = 0;
        return this;
    }

    getResult() {
        return this.value;
    }
}

const calc = new Calculator(10);
console.log(calc.add(5).multiply(2).getResult()); // 30

// تسلسل الطرق يجعل الكود أكثر قابلية للقراءة
const result = new Calculator(100)
    .subtract(20)
    .divide(4)
    .add(5)
    .getResult();
console.log(result); // 25

// الطرق على النموذج الأولي، وليس على كل نسخة
console.log(calc.hasOwnProperty('add')); // false
console.log(Calculator.prototype.hasOwnProperty('add')); // true

الطرق الثابتة

تعرف الطرق الثابتة باستخدام الكلمة المفتاحية static. إنها تنتمي للفئة نفسها، وليس لنسخ الفئة. تستدعيها مباشرة على الفئة، وليس على كائن منشأ من الفئة. تستخدم الطرق الثابتة عادة لدوال المساعدة المرتبطة بالفئة لكنها لا تحتاج الوصول إلى بيانات خاصة بالنسخة. لا يمكنك استدعاء طريقة ثابتة على نسخة -- ستطرح TypeError.

مثال: الطرق الثابتة

class MathUtils {
    static add(a, b) {
        return a + b;
    }

    static subtract(a, b) {
        return a - b;
    }

    static isEven(number) {
        return number % 2 === 0;
    }

    static clamp(value, min, max) {
        return Math.min(Math.max(value, min), max);
    }

    static randomBetween(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
}

// استدعاء الطرق الثابتة على الفئة نفسها
console.log(MathUtils.add(5, 3));       // 8
console.log(MathUtils.isEven(4));       // true
console.log(MathUtils.clamp(15, 0, 10)); // 10
console.log(MathUtils.randomBetween(1, 100)); // رقم عشوائي

// لا يمكن الاستدعاء على النسخ
const utils = new MathUtils();
// utils.add(1, 2); // TypeError: utils.add is not a function

الخصائص الثابتة

الخصائص الثابتة، مثل الطرق الثابتة، تنتمي للفئة نفسها بدلا من النسخ. إنها مفيدة لتعريف الثوابت وقيم التكوين أو العدادات التي يجب مشاركتها عبر جميع النسخ. يتم الوصول إلى الخصائص الثابتة باستخدام اسم الفئة.

مثال: الخصائص الثابتة

class Config {
    static API_URL = 'https://api.example.com';
    static MAX_RETRIES = 3;
    static VERSION = '2.1.0';
    static instanceCount = 0;

    constructor(name) {
        this.name = name;
        Config.instanceCount++;
    }

    static getInstanceCount() {
        return Config.instanceCount;
    }

    getApiUrl() {
        return Config.API_URL; // الوصول للخاصية الثابتة من طريقة النسخة
    }
}

console.log(Config.API_URL);     // "https://api.example.com"
console.log(Config.VERSION);     // "2.1.0"

const c1 = new Config('App1');
const c2 = new Config('App2');
console.log(Config.getInstanceCount()); // 2

console.log(c1.getApiUrl()); // "https://api.example.com"

المحصلات والمعدلات (Getters و Setters)

تسمح لك المحصلات والمعدلات بتعريف طرق يتم الوصول إليها كخصائص. المحصل يعمل عند قراءة قيمة خاصية، والمعدل يعمل عند تعيين قيمة لخاصية. يتم تعريفها باستخدام الكلمتين المفتاحيتين get وset. المحصلات والمعدلات مفيدة للخصائص المحسوبة والتحقق من الصحة والتحكم في الوصول إلى البيانات الداخلية.

مثال: المحصلات والمعدلات

class Temperature {
    constructor(celsius) {
        this._celsius = celsius; // اتفاقية الشرطة السفلية للخاصية "الخاصة"
    }

    // محصل: يتم الوصول إليه كخاصية، وليس كاستدعاء طريقة
    get fahrenheit() {
        return (this._celsius * 9 / 5) + 32;
    }

    // معدل: يتم تشغيله بالتعيين
    set fahrenheit(value) {
        this._celsius = (value - 32) * 5 / 9;
    }

    get celsius() {
        return this._celsius;
    }

    set celsius(value) {
        if (typeof value !== 'number') {
            throw new TypeError('درجة الحرارة يجب أن تكون رقما');
        }
        if (value < -273.15) {
            throw new RangeError('درجة الحرارة تحت الصفر المطلق');
        }
        this._celsius = value;
    }

    get kelvin() {
        return this._celsius + 273.15;
    }

    toString() {
        return this._celsius.toFixed(1) + ' درجة مئوية';
    }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit); // 212 (بدون أقواس -- إنه محصل)
console.log(temp.kelvin);     // 373.15

// استخدام المعدل بتعيين قيمة
temp.fahrenheit = 32;
console.log(temp.celsius); // 0

// التحقق من المعدل في العمل
try {
    temp.celsius = -300; // تحت الصفر المطلق
} catch (error) {
    console.log(error.message); // "درجة الحرارة تحت الصفر المطلق"
}

console.log(temp.toString()); // "0.0 درجة مئوية"
نصيحة احترافية: المحصلات والمعدلات طريقة رائعة للحفاظ على التوافق العكسي. إذا بدأت بخاصية عامة بسيطة واحتجت لاحقا لإضافة تحقق أو حساب، يمكنك استبدالها بمحصل ومعدل دون تغيير الكود الذي يستخدم الفئة. تبقى الواجهة البرمجية الخارجية كما هي.

تعبيرات الفئة

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

مثال: تعبيرات الفئة

// تعبير فئة غير مسمى
const Animal = class {
    constructor(name) {
        this.name = name;
    }

    speak() {
        return this.name + ' يصدر صوتا.';
    }
};

const dog = new Animal('Rex');
console.log(dog.speak()); // "Rex يصدر صوتا."

// تعبير فئة مسمى
const Vehicle = class Car {
    constructor(brand) {
        this.brand = brand;
    }

    identify() {
        // 'Car' متاح فقط داخل جسم الفئة
        return 'هذه ' + Car.name + ': ' + this.brand;
    }
};

const v = new Vehicle('Toyota');
console.log(v.identify()); // "هذه Car: Toyota"
// console.log(Car); // ReferenceError: Car is not defined (خارج الفئة)

// يمكن تمرير الفئات كوسائط
function createInstance(ClassRef, args) {
    return new ClassRef(args);
}

const myDog = createInstance(Animal, 'Buddy');
console.log(myDog.speak()); // "Buddy يصدر صوتا."

وراثة الفئات باستخدام extends

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

مثال: الوراثة الأساسية

class Animal {
    constructor(name, sound) {
        this.name = name;
        this.sound = sound;
    }

    speak() {
        return this.name + ' يقول ' + this.sound + '!';
    }

    describe() {
        return 'أنا ' + this.name + '، حيوان.';
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name, 'هاو'); // استدعاء باني الأب
        this.tricks = [];
    }

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

    showTricks() {
        if (this.tricks.length === 0) {
            return this.name + ' ليس لديه حيل بعد.';
        }
        return this.name + ' يستطيع: ' + this.tricks.join('، ') + '.';
    }
}

class Cat extends Animal {
    constructor(name) {
        super(name, 'مياو');
        this.indoor = true;
    }

    purr() {
        return this.name + ' يخرخر بارتياح.';
    }
}

const dog = new Dog('Rex');
console.log(dog.speak());    // "Rex يقول هاو!" (موروثة)
console.log(dog.describe()); // "أنا Rex، حيوان." (موروثة)
dog.learnTrick('اجلس');
dog.learnTrick('صافح');
console.log(dog.showTricks()); // "Rex يستطيع: اجلس، صافح." (طريقة خاصة)

const cat = new Cat('Whiskers');
console.log(cat.speak()); // "Whiskers يقول مياو!" (موروثة)
console.log(cat.purr());  // "Whiskers يخرخر بارتياح." (طريقة خاصة)

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

تستخدم الكلمة المفتاحية super في سياقين داخل فئة ابن. أولا، super() في الباني يستدعي باني الفئة الأب. هذا إلزامي إذا كان للفئة الابن باني -- يجب عليك استدعاء super() قبل استخدام this، وإلا ستحصل على ReferenceError. ثانيا، super.methodName() يستدعي طريقة من الفئة الأب، وهو مفيد عندما تريد توسيع سلوك الأب بدلا من استبداله بالكامل.

مثال: استخدام super في البناة والطرق

class Shape {
    constructor(color) {
        this.color = color;
    }

    describe() {
        return 'شكل ' + this.color;
    }

    area() {
        return 0; // التنفيذ الأساسي
    }
}

class Circle extends Shape {
    constructor(color, radius) {
        // يجب استدعاء super() قبل استخدام 'this'
        super(color);
        this.radius = radius;
    }

    area() {
        return Math.PI * this.radius * this.radius;
    }

    describe() {
        // استدعاء describe() للأب وتوسيعها
        return super.describe() + ' (دائرة نصف قطرها ' + this.radius + ')';
    }
}

class Rectangle extends Shape {
    constructor(color, width, height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    area() {
        return this.width * this.height;
    }

    perimeter() {
        return 2 * (this.width + this.height);
    }

    describe() {
        return super.describe() + ' (مستطيل ' + this.width + 'x' + this.height + ')';
    }
}

const circle = new Circle('أحمر', 5);
console.log(circle.describe()); // "شكل أحمر (دائرة نصف قطرها 5)"
console.log(circle.area().toFixed(2)); // "78.54"

const rect = new Rectangle('أزرق', 10, 5);
console.log(rect.describe());   // "شكل أزرق (مستطيل 10x5)"
console.log(rect.area());       // 50
console.log(rect.perimeter());  // 30
خطأ شائع: نسيان استدعاء super() في باني الفئة الابن، أو محاولة استخدام this قبل استدعاء super(). كلاهما سيؤدي إلى ReferenceError. إذا لم تكن فئتك الابن تحتاج إلى باني على الإطلاق، يمكنك حذفه بالكامل وسيتم استدعاء باني الأب تلقائيا.

تجاوز الطرق

عندما تعرف فئة ابن طريقة بنفس اسم طريقة في الفئة الأب، فإن طريقة الابن تتجاوز طريقة الأب. سيتم استدعاء نسخة الابن عند استدعاء الطريقة على نسخ من الفئة الابن. لا يزال بإمكانك الوصول إلى نسخة الأب باستخدام super.methodName() إذا كنت تريد دمج سلوك الأب مع سلوك جديد.

مثال: أنماط تجاوز الطرق

class Logger {
    log(message) {
        console.log('[سجل] ' + message);
    }

    formatDate() {
        return new Date().toISOString();
    }

    createEntry(level, message) {
        return this.formatDate() + ' [' + level + '] ' + message;
    }
}

class FileLogger extends Logger {
    constructor(filename) {
        super();
        this.filename = filename;
        this.entries = [];
    }

    // تجاوز كامل لطريقة log
    log(message) {
        const entry = this.createEntry('معلومات', message);
        this.entries.push(entry);
        // أيضا استدعاء log للأب لإخراج وحدة التحكم
        super.log(message);
    }

    // تجاوز مع سلوك إضافي
    formatDate() {
        // استخدام تنسيق أبسط من الأب
        const now = new Date();
        return now.toLocaleDateString() + ' ' + now.toLocaleTimeString();
    }

    getEntries() {
        return this.entries;
    }
}

class ErrorLogger extends FileLogger {
    constructor(filename) {
        super(filename);
    }

    // تجاوز log لاستخدام مستوى خطأ دائما
    log(message) {
        const entry = this.createEntry('خطأ', message);
        this.entries.push(entry);
        console.error('[خطأ] ' + message);
    }

    // إضافة طريقة جديدة خاصة بـ ErrorLogger
    logWithStack(message) {
        const stack = new Error().stack;
        this.log(message + '\nالمكدس: ' + stack);
    }
}

const fileLog = new FileLogger('app.log');
fileLog.log('بدأ التطبيق');
fileLog.log('سجل المستخدم دخوله');
console.log(fileLog.getEntries().length); // 2

const errorLog = new ErrorLogger('error.log');
errorLog.log('فشل الاتصال');

عامل instanceof

يتحقق عامل instanceof مما إذا كان كائن هو نسخة من فئة معينة. يرجع أيضا true للفئات الأب في سلسلة الوراثة. هذا مفيد للتحقق من النوع، خاصة عندما يكون لديك تسلسل هرمي من الفئات وتحتاج لتحديد نوع الكائن الذي تعمل معه.

مثال: استخدام instanceof

class Vehicle {
    constructor(type) {
        this.type = type;
    }
}

class Car extends Vehicle {
    constructor(brand) {
        super('سيارة');
        this.brand = brand;
    }
}

class ElectricCar extends Car {
    constructor(brand, range) {
        super(brand);
        this.range = range;
    }
}

const tesla = new ElectricCar('Tesla', 350);

console.log(tesla instanceof ElectricCar); // true
console.log(tesla instanceof Car);         // true
console.log(tesla instanceof Vehicle);     // true
console.log(tesla instanceof Object);      // true (كل شيء يرث من Object)

const honda = new Car('Honda');
console.log(honda instanceof Car);         // true
console.log(honda instanceof ElectricCar); // false (الأب ليس نسخة من الابن)

// استخدام عملي: منطق قائم على النوع
function describeVehicle(vehicle) {
    if (vehicle instanceof ElectricCar) {
        return vehicle.brand + ' (كهربائية، المدى: ' + vehicle.range + ' ميل)';
    } else if (vehicle instanceof Car) {
        return vehicle.brand + ' (سيارة عادية)';
    } else if (vehicle instanceof Vehicle) {
        return 'مركبة من نوع ' + vehicle.type;
    }
    return 'مركبة غير معروفة';
}

console.log(describeVehicle(tesla)); // "Tesla (كهربائية، المدى: 350 ميل)"
console.log(describeVehicle(honda)); // "Honda (سيارة عادية)"

حقول الفئة العامة والخاصة

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

مثال: الحقول العامة والخاصة

class BankAccount {
    // حقول عامة بقيم افتراضية
    owner = 'غير معروف';
    accountType = 'جاري';

    // حقول خاصة -- يمكن الوصول إليها فقط داخل الفئة
    #balance = 0;
    #transactionHistory = [];
    #pin;

    constructor(owner, initialBalance, pin) {
        this.owner = owner;
        this.#balance = initialBalance;
        this.#pin = pin;
        this.#recordTransaction('تم فتح الحساب', initialBalance);
    }

    // طريقة عامة
    deposit(amount) {
        if (amount <= 0) {
            throw new Error('مبلغ الإيداع يجب أن يكون موجبا');
        }
        this.#balance += amount;
        this.#recordTransaction('إيداع', amount);
        return this.#balance;
    }

    withdraw(amount, pin) {
        this.#verifyPin(pin);
        if (amount > this.#balance) {
            throw new Error('رصيد غير كاف');
        }
        this.#balance -= amount;
        this.#recordTransaction('سحب', -amount);
        return this.#balance;
    }

    getBalance(pin) {
        this.#verifyPin(pin);
        return this.#balance;
    }

    getStatement() {
        return this.#transactionHistory.map(function(t) {
            return t.date + ': ' + t.description + ' (' + t.amount + ')';
        }).join('\n');
    }

    // طرق خاصة
    #verifyPin(pin) {
        if (pin !== this.#pin) {
            throw new Error('رقم سري غير صالح');
        }
    }

    #recordTransaction(description, amount) {
        this.#transactionHistory.push({
            date: new Date().toISOString(),
            description: description,
            amount: amount
        });
    }
}

const account = new BankAccount('Ahmed', 1000, '1234');
account.deposit(500);
console.log(account.getBalance('1234')); // 1500
account.withdraw(200, '1234');
console.log(account.getBalance('1234')); // 1300

// الحقول العامة يمكن الوصول إليها
console.log(account.owner); // "Ahmed"

// الحقول الخاصة لا يمكن الوصول إليها من الخارج
// console.log(account.#balance);   // SyntaxError
// console.log(account.#pin);       // SyntaxError
// account.#verifyPin('1234');      // SyntaxError
ملاحظة: الحقول الخاصة بـ # هي إضافة حديثة نسبيا إلى JavaScript (ES2022). إنها مدعومة في جميع المتصفحات الحديثة و Node.js 12+. على عكس اتفاقية تسمية الشرطة السفلية (_balance)، فإن حقول # مفروضة بواسطة محرك JavaScript -- لا توجد طريقة للوصول إليها من خارج الفئة، مما يجعلها خاصة حقا.

الطرق الخاصة

تعمل الطرق الخاصة بنفس طريقة الحقول الخاصة -- مسبوقة بـ # ويمكن استدعاؤها فقط من داخل جسم الفئة. إنها مثالية لدوال المساعدة الداخلية التي لا يجب أن تكون جزءا من الواجهة البرمجية العامة.

مثال: الطرق الخاصة في الممارسة

class PasswordValidator {
    #minLength;
    #requireUppercase;
    #requireNumbers;
    #requireSpecial;

    constructor(options) {
        this.#minLength = options.minLength || 8;
        this.#requireUppercase = options.requireUppercase !== false;
        this.#requireNumbers = options.requireNumbers !== false;
        this.#requireSpecial = options.requireSpecial || false;
    }

    // طريقة عامة -- الواجهة البرمجية الوحيدة المكشوفة
    validate(password) {
        const errors = [];

        if (!this.#checkLength(password)) {
            errors.push('يجب أن يكون ' + this.#minLength + ' أحرف على الأقل');
        }
        if (this.#requireUppercase && !this.#checkUppercase(password)) {
            errors.push('يجب أن يحتوي على حرف كبير');
        }
        if (this.#requireNumbers && !this.#checkNumbers(password)) {
            errors.push('يجب أن يحتوي على رقم');
        }
        if (this.#requireSpecial && !this.#checkSpecialChars(password)) {
            errors.push('يجب أن يحتوي على حرف خاص');
        }

        return {
            isValid: errors.length === 0,
            errors: errors,
            strength: this.#calculateStrength(password)
        };
    }

    // طرق مساعدة خاصة
    #checkLength(password) {
        return password.length >= this.#minLength;
    }

    #checkUppercase(password) {
        return /[A-Z]/.test(password);
    }

    #checkNumbers(password) {
        return /[0-9]/.test(password);
    }

    #checkSpecialChars(password) {
        return /[!@#$%^&*(),.?":{}|]/.test(password);
    }

    #calculateStrength(password) {
        let score = 0;
        if (password.length >= 8) score++;
        if (password.length >= 12) score++;
        if (/[A-Z]/.test(password)) score++;
        if (/[0-9]/.test(password)) score++;
        if (/[^A-Za-z0-9]/.test(password)) score++;

        if (score <= 2) return 'ضعيفة';
        if (score <= 3) return 'متوسطة';
        return 'قوية';
    }
}

const validator = new PasswordValidator({
    minLength: 10,
    requireSpecial: true
});

const result1 = validator.validate('hello');
console.log(result1);
// { isValid: false, errors: [...], strength: "ضعيفة" }

const result2 = validator.validate('MyStr0ng!Pass');
console.log(result2);
// { isValid: true, errors: [], strength: "قوية" }

الفئات مقابل دوال البناء

فهم العلاقة بين الفئات ودوال البناء يساعدك على تقدير ما توفره الفئات وكيفية العمل مع كود JavaScript القديم. إليك مقارنة جنبا إلى جنب توضح كيف يتم التعبير عن نفس الوظيفة في كلا الأسلوبين.

مثال: الفئة مقابل دالة البناء

// ===== نهج دالة البناء (ES5) =====
function PersonES5(name, age) {
    this.name = name;
    this.age = age;
}

PersonES5.prototype.greet = function() {
    return 'مرحبا، أنا ' + this.name;
};

PersonES5.create = function(name, age) {
    return new PersonES5(name, age);
};

// الوراثة مع دوال البناء
function StudentES5(name, age, grade) {
    PersonES5.call(this, name, age); // استدعاء باني الأب
    this.grade = grade;
}

StudentES5.prototype = Object.create(PersonES5.prototype);
StudentES5.prototype.constructor = StudentES5;

StudentES5.prototype.study = function() {
    return this.name + ' يدرس.';
};

// ===== نهج الفئات (ES6+) =====
class PersonES6 {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    greet() {
        return 'مرحبا، أنا ' + this.name;
    }

    static create(name, age) {
        return new PersonES6(name, age);
    }
}

class StudentES6 extends PersonES6 {
    constructor(name, age, grade) {
        super(name, age);
        this.grade = grade;
    }

    study() {
        return this.name + ' يدرس.';
    }
}

// كلاهما ينتج نفس النتائج
const s1 = new StudentES5('Ali', 20, 'A');
const s2 = new StudentES6('Ali', 20, 'A');

console.log(s1.greet());  // "مرحبا، أنا Ali"
console.log(s2.greet());  // "مرحبا، أنا Ali"
console.log(s1.study());  // "Ali يدرس."
console.log(s2.study());  // "Ali يدرس."
نصيحة احترافية: فضل دائما صيغة فئات ES6 للكود الجديد. إنها أنظف وأقل عرضة للأخطاء ومفهومة عالميا. لا يزال نمط دالة البناء موجودا في المكتبات والأكواد القديمة، لذا فإن الإلمام بكليهما قيم، لكن لا يوجد سبب لاستخدام دوال البناء في JavaScript الحديث.

مثال واقعي: فئة المستخدم

لنبنِ فئة مستخدم عملية قد تجدها في تطبيق ويب حقيقي. إنها توضح البناة والطرق والمحصلات والمعدلات والطرق الثابتة والحقول الخاصة تعمل معا.

مثال: فئة المستخدم الكاملة

class User {
    static #nextId = 1;
    static roles = ['user', 'editor', 'admin'];

    #id;
    #password;
    #loginAttempts = 0;
    #locked = false;

    constructor(username, email, password, role) {
        this.#id = User.#nextId++;
        this.username = username;
        this.email = email;
        this.#password = password;
        this.role = User.roles.includes(role) ? role : 'user';
        this.createdAt = new Date();
        this.lastLogin = null;
    }

    get id() {
        return this.#id;
    }

    get isLocked() {
        return this.#locked;
    }

    get displayName() {
        return '@' + this.username;
    }

    set email(value) {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
            throw new Error('تنسيق بريد إلكتروني غير صالح: ' + value);
        }
        this._email = value;
    }

    get email() {
        return this._email;
    }

    authenticate(password) {
        if (this.#locked) {
            throw new Error('الحساب مقفل. اتصل بالدعم.');
        }

        if (password === this.#password) {
            this.#loginAttempts = 0;
            this.lastLogin = new Date();
            return true;
        }

        this.#loginAttempts++;
        if (this.#loginAttempts >= 5) {
            this.#locked = true;
            throw new Error('تم قفل الحساب بعد 5 محاولات فاشلة.');
        }

        return false;
    }

    changePassword(oldPassword, newPassword) {
        if (oldPassword !== this.#password) {
            throw new Error('كلمة المرور الحالية غير صحيحة');
        }
        if (newPassword.length < 8) {
            throw new Error('كلمة المرور الجديدة يجب أن تكون 8 أحرف على الأقل');
        }
        this.#password = newPassword;
    }

    hasPermission(action) {
        const permissions = {
            user: ['read'],
            editor: ['read', 'write', 'edit'],
            admin: ['read', 'write', 'edit', 'delete', 'manage']
        };
        return permissions[this.role].includes(action);
    }

    toJSON() {
        return {
            id: this.#id,
            username: this.username,
            email: this.email,
            role: this.role,
            createdAt: this.createdAt,
            lastLogin: this.lastLogin
        };
    }

    static findByEmail(users, email) {
        return users.find(function(user) {
            return user.email === email;
        }) || null;
    }
}

const admin = new User('ahmed', 'ahmed@example.com', 'SecurePass1', 'admin');
console.log(admin.id);          // 1
console.log(admin.displayName); // "@ahmed"
console.log(admin.hasPermission('delete')); // true

admin.authenticate('SecurePass1');
console.log(admin.lastLogin); // التاريخ الحالي

console.log(JSON.stringify(admin.toJSON(), null, 2));

مثال واقعي: المنتج وسلة التسوق

إليك مثال أكثر تعقيدا يوضح كيف تعمل فئات متعددة معا. فئة Product تمثل العناصر، وفئة ShoppingCart تدير مجموعة من المنتجات بكمياتها. هذا يوضح الوراثة والتركيب والطرق الثابتة والتغليف في سيناريو تجارة إلكترونية عملي.

مثال: فئات المنتج وسلة التسوق

class Product {
    #id;
    #price;
    static #catalog = [];

    constructor(id, name, price, category) {
        this.#id = id;
        this.name = name;
        this.#price = price;
        this.category = category;
        this.inStock = true;
        Product.#catalog.push(this);
    }

    get id() {
        return this.#id;
    }

    get price() {
        return this.#price;
    }

    set price(newPrice) {
        if (newPrice < 0) {
            throw new Error('السعر لا يمكن أن يكون سالبا');
        }
        this.#price = newPrice;
    }

    getFormattedPrice() {
        return '$' + this.#price.toFixed(2);
    }

    toString() {
        return this.name + ' (' + this.getFormattedPrice() + ')';
    }

    static getCatalog() {
        return [...Product.#catalog];
    }

    static findById(id) {
        return Product.#catalog.find(function(p) {
            return p.id === id;
        }) || null;
    }

    static findByCategory(category) {
        return Product.#catalog.filter(function(p) {
            return p.category === category;
        });
    }
}

class DigitalProduct extends Product {
    #downloadUrl;
    #fileSize;

    constructor(id, name, price, category, downloadUrl, fileSize) {
        super(id, name, price, category);
        this.#downloadUrl = downloadUrl;
        this.#fileSize = fileSize;
    }

    getDownloadLink() {
        return this.#downloadUrl;
    }

    getFileSize() {
        if (this.#fileSize < 1024) {
            return this.#fileSize + ' كيلوبايت';
        }
        return (this.#fileSize / 1024).toFixed(1) + ' ميغابايت';
    }

    toString() {
        return super.toString() + ' [رقمي - ' + this.getFileSize() + ']';
    }
}

class ShoppingCart {
    #items = [];
    #discountCode = null;
    #discountPercent = 0;

    addItem(product, quantity) {
        if (quantity === undefined) quantity = 1;
        if (!(product instanceof Product)) {
            throw new Error('العنصر يجب أن يكون نسخة من Product');
        }
        if (!product.inStock) {
            throw new Error(product.name + ' غير متوفر');
        }

        const existing = this.#items.find(function(item) {
            return item.product.id === product.id;
        });

        if (existing) {
            existing.quantity += quantity;
        } else {
            this.#items.push({ product: product, quantity: quantity });
        }

        return this;
    }

    removeItem(productId) {
        this.#items = this.#items.filter(function(item) {
            return item.product.id !== productId;
        });
        return this;
    }

    applyDiscount(code, percent) {
        this.#discountCode = code;
        this.#discountPercent = Math.min(percent, 50);
        return this;
    }

    get subtotal() {
        return this.#items.reduce(function(sum, item) {
            return sum + (item.product.price * item.quantity);
        }, 0);
    }

    get discount() {
        return this.subtotal * (this.#discountPercent / 100);
    }

    get total() {
        return this.subtotal - this.discount;
    }

    get itemCount() {
        return this.#items.reduce(function(count, item) {
            return count + item.quantity;
        }, 0);
    }

    getSummary() {
        let summary = 'سلة التسوق (' + this.itemCount + ' عناصر):\n';
        summary += '----------------------------\n';
        this.#items.forEach(function(item) {
            const lineTotal = item.product.price * item.quantity;
            summary += item.product.name + ' x' + item.quantity;
            summary += ' = $' + lineTotal.toFixed(2) + '\n';
        });
        summary += '----------------------------\n';
        summary += 'المجموع الفرعي: $' + this.subtotal.toFixed(2) + '\n';
        if (this.#discountPercent > 0) {
            summary += 'الخصم (' + this.#discountCode + '): -$';
            summary += this.discount.toFixed(2) + '\n';
        }
        summary += 'الإجمالي: $' + this.total.toFixed(2);
        return summary;
    }

    clear() {
        this.#items = [];
        this.#discountCode = null;
        this.#discountPercent = 0;
    }
}

// الاستخدام
const laptop = new Product(1, 'حاسب محمول', 999.99, 'إلكترونيات');
const mouse = new Product(2, 'فأرة', 29.99, 'إلكترونيات');
const ebook = new DigitalProduct(3, 'دليل JS', 19.99, 'كتب',
    'https://example.com/download/js-guide', 2500);

const cart = new ShoppingCart();
cart.addItem(laptop, 1)
    .addItem(mouse, 2)
    .addItem(ebook, 1)
    .applyDiscount('SAVE10', 10);

console.log(cart.getSummary());

console.log(ebook.toString()); // "دليل JS ($19.99) [رقمي - 2.4 ميغابايت]"

// استخدام الطرق الثابتة
console.log(Product.findByCategory('إلكترونيات').length); // 2

سلسلة الوراثة والوراثة متعددة المستويات

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

مثال: الوراثة متعددة المستويات

class Component {
    constructor(id) {
        this.id = id;
        this.visible = true;
    }

    show() {
        this.visible = true;
        return this;
    }

    hide() {
        this.visible = false;
        return this;
    }

    toString() {
        return 'Component#' + this.id;
    }
}

class InteractiveComponent extends Component {
    #eventHandlers = {};

    constructor(id) {
        super(id);
        this.enabled = true;
    }

    on(event, handler) {
        if (!this.#eventHandlers[event]) {
            this.#eventHandlers[event] = [];
        }
        this.#eventHandlers[event].push(handler);
        return this;
    }

    trigger(event, data) {
        const handlers = this.#eventHandlers[event] || [];
        handlers.forEach(function(handler) {
            handler(data);
        });
        return this;
    }

    disable() {
        this.enabled = false;
        return this;
    }

    enable() {
        this.enabled = true;
        return this;
    }
}

class Button extends InteractiveComponent {
    constructor(id, label) {
        super(id);
        this.label = label;
    }

    click() {
        if (this.enabled && this.visible) {
            this.trigger('click', { buttonId: this.id, label: this.label });
        }
    }

    toString() {
        return 'Button#' + this.id + ' (' + this.label + ')';
    }
}

const submitBtn = new Button('btn-1', 'إرسال');

submitBtn.on('click', function(data) {
    console.log('تم النقر على الزر: ' + data.label);
});

submitBtn.click(); // "تم النقر على الزر: إرسال"
submitBtn.disable();
submitBtn.click(); // لا شيء يحدث -- الزر معطل
submitBtn.enable();
submitBtn.click(); // "تم النقر على الزر: إرسال"

// التحقق من سلسلة الوراثة
console.log(submitBtn instanceof Button);               // true
console.log(submitBtn instanceof InteractiveComponent); // true
console.log(submitBtn instanceof Component);            // true
console.log(submitBtn.toString()); // "Button#btn-1 (إرسال)"
تحذير: تجنب سلاسل الوراثة العميقة (أكثر من ثلاثة مستويات). التسلسلات الهرمية العميقة تصبح صعبة الفهم والتصحيح والصيانة. إذا وجدت نفسك تنشئ مستويات كثيرة من الوراثة، فكر في استخدام التركيب بدلا من ذلك -- حيث تحتوي الفئة على نسخ من فئات أخرى بدلا من الامتداد منها. المقولة الشائعة في تصميم البرمجيات هي "فضل التركيب على الوراثة."
نصيحة احترافية: عند تصميم الفئات، اتبع مبدأ المسؤولية الواحدة: كل فئة يجب أن يكون لها غرض واضح واحد. فئة User تتعامل مع بيانات المستخدم والمصادقة. فئة ShoppingCart تتعامل مع عمليات السلة. فئة Product تتعامل مع معلومات المنتج. قاوم الرغبة في وضع وظائف غير مرتبطة في فئة واحدة لمجرد أنها تبدو مريحة.

تمرين عملي

ابنِ نظام إدارة مهام باستخدام فئات ES6. أنشئ التسلسل الهرمي التالي من الفئات: (1) فئة أساسية Task بحقول خاصة للمعرف والعنوان والوصف والحالة (معلقة، قيد التنفيذ، مكتملة) والأولوية (منخفضة، متوسطة، عالية) وتاريخ الإنشاء وتاريخ الإكمال. تتضمن طرقا لتحديث الحالة وتغيير الأولوية وطريقة toString. استخدم محصلات لجميع الحقول الخاصة ومعدلات مع التحقق من الصحة للحالة والأولوية. (2) فئة TimedTask تمتد من Task وتضيف موعدا نهائيا وطريقة للتحقق مما إذا كانت المهمة متأخرة وطريقة للحصول على الوقت المتبقي. (3) فئة RecurringTask تمتد من Task وتضيف نمط تكرار (يومي، أسبوعي، شهري) وطريقة لتوليد الحدث التالي وعداد لعدد مرات الإكمال. (4) فئة TaskBoard تدير مجموعة من المهام بطرق لإضافة المهام وإزالة المهام والتصفية حسب الحالة والتصفية حسب الأولوية والفرز حسب الموعد النهائي أو الأولوية والحصول على إحصائيات (الإجمالي، المكتملة، المتأخرة) والبحث حسب العنوان أو الوصف. استخدم الحقول الخاصة والطرق الثابتة لإنشاء مهام بمعرفات متزايدة تلقائيا وتسلسل الطرق حيثما كان مناسبا. اختبر نظامك بإنشاء مهام متعددة من أنواع مختلفة وتغيير حالاتها وتصفية وفرز لوحة المهام والتحقق من أن الحقول الخاصة لا يمكن الوصول إليها من خارج الفئات.