تحقيق ميكانيكية الوراثة ECMAScript

تحقيق ميكانيكية الإرث

لتحقيق ميكانيكية الإرث في ECMAScript، يمكنك البدء بالكلاس الأساسي الذي ترغب في الإرث منه. جميع الكلاسات التي تم تعريفها من قبل المطورين يمكن أن تكون كلاسًا أساسيًا. لسبب أمني، لا يمكن استخدام الكلاسات المحلية والكلاسات المستضيفة ككلاسات أساسية، مما يمنع الوصول العام إلى الكود المترجم للبrowsers، لأن هذا الكود يمكن استخدامه في الهجمات المكرسة.

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

على الرغم من أن ECMAScript لم يحدد بشكل صارم الكلاسات التجريبية مثل لغات البرمجة الأخرى، إلا أنني أحيانًا يُقام بإنشاء بعض الكلاسات التي لا يُسمح استخدامها. عادة ما نسمي هذه الكلاسات كلاسات تجريبية.

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

طرق الإرث

مثل أي وظيفة أخرى، هناك أكثر من طريقة لتحقيق الإرث في ECMAScript. هذا لأن آلية الإرث في JavaScript ليست محددة بشكل صريح، بل يتم تحقيقها عن طريق محاكاة. هذا يعني أن تفاصيل الإرث ليست معالجة تمامًا من قبل البرنامج المفسير. كمطور، لديك الحق في اتخاذ القرار الأنسب لطريقة الإرث.

سنقدم لك فيما بعد بعض الطرق المحددة للإرث.

مزاحة الأجسام

عند تصميم ECMAScript الأصلي، لم يكن هناك نية لتصميم مزاحة الأجسام (object masquerading). أنها تطورت بعد أن أدرك المطورون كيفية عمل الدوال، خاصة كيفية استخدام الكلمة المفتاحية this في بيئة الدالة.

المنطق وراء ذلك هو: يستخدم بناء الدالة هذا الكلمة المفتاحية this لتعيين القيم لجميع السمات والأساليب (أي استخدام بناء الفئة). لأن بناء الدالة هو مجرد دالة، يمكن أن يجعل بناء ClassA دالة لـ ClassB، ثم يمكن استدعاؤها. ستحصل ClassB على السمات والأساليب المحددة في بناء ClassA. على سبيل المثال، يمكن تعريف ClassA و ClassB كما يلي:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

هل تتذكر؟ الكلمة المفتاحية this تشير إلى الجسم الحالي الذي يتم إنشاؤه من قبل البناء. ولكن في هذا الأسلوب، يشير this إلى الجسم الذي ينتمي إليه. هذا المبدأ هو إقامة آلية الإرث كأسلوب عادي باستخدام ClassA بدلاً من كونه بناءً. يمكن تحقيق آلية الإرث باستخدام بناء ClassB كما يلي:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

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

جميع السمات الجديدة والأساليب الجديدة يجب أن تكون معرفة بعد إزالة سطر الكود الجديد للأساليب. وإلا قد يتم تغطية السمات والأساليب المرتبطة بالفئة الأبوية:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

لإثبات أن الكود السابق فعال، يمكن تشغيل الأمثلة التالية:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//输出 "blue"
objB.sayColor();	//输出 "red"
objB.sayName();		//يتم إصدار "John"

يمكن للمزاحة بالأجسام تحقيق الإرث المتعدد

من المثير للاهتمام أن يمكن للمزاحة بالأجسام دعم الإرث المتعدد. أي أن الفئة يمكن أن تورث من أكثر من فئة آباء. يمكن عرض آلية الإرث المتعدد التي تمثلها UML كما يلي:

هيكل الوراثة نموذج رسوم بيانية UML

على سبيل المثال، إذا كان هناك اثنان من الفئات ClassX و ClassY، فإن ClassZ يريد الإرث من هاتين الفئتين، يمكن استخدام الكود التالي:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

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

بسبب شعبية هذه الطريقة للتكامل عبر التركة،أضافت النسخة الثالثة من ECMAScript طريقتين جديدتين إلى جسم Function،هما call() و apply().

طريقة call()

طريقة call() هي أكثر تشابهًا بطريقة التكامل عبر التركة التقليدية.المعلمة الأولى تستخدم كـ this كجسم.المعلمات الأخرى يتم نقلها مباشرة إلى الدالة نفسها.على سبيل المثال:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
عندما يتم إنشاء obj كجسم جديد من Object();
obj.color = ";blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

في هذا المثال،تم تعريف دالة sayColor() خارج الجسم،حتى لو لم تكن تنتمي إلى أي جسم،يمكن استخدام الكلمة المفتاحية this.تساوي خاصية color في جسم obj كأزرق.عند تشغيل دالة call()،المعلمة الأولى هي obj،ما يعني أن يجب تعيين هذا لـ this في دالة sayColor().المعلمتان الثانية والثالثة هما كلمتان.تتناسب مع المعلمتان sPrefix و sSuffix في دالة sayColor().سيتم عرض الرسالة النهائية "The color is blue, a very nice color indeed.".

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

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

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

طريقة apply()

للمعرفة،يملك apply() طريقتين،وذلك كي يتم استخدام this كجسم،وإرسال مجموعة من المعلمات إلى الدالة.على سبيل المثال:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
عندما يتم إنشاء obj كجسم جديد من Object();
obj.color = ";blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

هذا المثال مشابه للمعاينة السابقة، ولكن الآن يتم استدعاء apply()

يستخدم هذا الأسلوب أيضًا لتعويض الأوامر الثلاثة الأولى في الأسطر الثلاثة الأولى من الassignment وcall وdelete للمетод الجديد:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

بالطبع، يبقى المعلم الأول هو this، والمعلم الثاني هو مصفوفة تحتوي على قيمة واحدة فقط color. يمكن نقل كامل كائن arguments من ClassB كمعلم ثاني لـ apply()

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

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

prototype chaining

يستخدم هذا النوع من الوراثة في ECMAScript أصلاً لـ prototype chaining. في الفصل السابق تم تقديم طريقة تعريف الفئة باستخدام prototype. يتم توسيع هذا الأسلوب بطرق مثيرة للاهتمام لتحقيق ميكانيكية الوراثة.

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

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

function ClassA() {
}
ClassA.prototype.color = ";blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

المعجزة في الطريقة النموذجية تكمن في سطر الكود المظلل بالأزرق. هنا، يتم تعيين خاصية prototype للفئة ClassB إلى نموذج ClassA. هذا مثير للاهتمام، لأنك تريد أن تحصل على جميع خصائص وأساليب ClassA، ولكن لا تريد تخصيصها يدويًا في خاصية prototype للفئة ClassB. هل هناك طريقة أفضل من تعيين نموذج ClassA إلى خاصية prototype؟

ملاحظة:يتم استدعاء مكون الفئة ClassA دون إرسال أي معلمات إليه. هذا هو الممارسة القياسية في سلسلة النماذج. تأكد من عدم وجود أي معلمات لـ مكون الفئة.

مثل التظاهر بالأشياء، يجب أن تكون جميع خصائص الفئة وأساليبها موجودة في prototype بعد تخصيصها، لأن أي أساليب تم تخصيصها قبل ذلك سيتم حذفها. لماذا؟ لأن خاصية prototype تم استبدالها بنوع جديد من الأشياء، ويتم تدمير الأشياء الأصلية التي تم إضافة الأساليب إليها. لذا، فإن الكود التالي لإضافة خاصية name وأسلوب sayName() للفئة ClassB هو:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

يمكنك اختبار هذا الكود من خلال تشغيل المثال التالي:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

بالإضافة إلى ذلك، في سلسلة النماذج، يعمل عميل instanceof بطريقة فريدة. لجميع حالات ClassB، يعود instanceof إلى ClassA و ClassB كلاهما كـ true. على سبيل المثال:

var objB = new ClassB();
alert(objB instanceof ClassA);	//يظهر "true"
alert(objB instanceof ClassB);	//يظهر "true"

في عالم ECMAScript من الأنواع الضعيفة، هذا أداة مفيدة للغاية، ولكن لا يمكنك استخدامها عند التظاهر بالأشياء.

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

الطريقة المختلطة

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

在前一章,我们曾经讲解过创建类的最好方式是用构造函数定义属性,用原型定义方法。这种方式同样适用于继承机制,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象的方法。用这两种方式重写前面的例子,代码如下:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

在此例子中,继承机制由两行突出显示的蓝色代码实现。在第一行突出显示的代码中,在 ClassB 构造函数中,用对象冒充继承 ClassA 类的 sColor 属性。在第二行突出显示的代码中,用原型链继承 ClassA 类的方法。由于这种混合方式使用了原型链,所以 instanceof 运算符仍能正确运行。

下面的例子测试了这段代码:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//输出 "blue"
objB.sayColor();	//输出 "red"
objB.sayName();	//输出 "John"

TIY