في البرمجة، غالبًا ما نرغب في أخذ شيء ما وتوسيع نطاقه.
على سبيل المثال، لدينا كائن user
بخصائصه وأساليبه، ونريد أن نجعل admin
و guest
متغيرات معدلة قليلاً منه. نود إعادة استخدام ما لدينا في user
، وليس نسخ/إعادة تنفيذ أساليبه، بل نرغب فقط في إنشاء كائن جديد فوقه.
الميراث النموذجي هو ميزة لغوية تساعد في ذلك.
في JavaScript، الكائنات لها خاصية مخفية خاصة [[Prototype]]
(كما هو مذكور في المواصفات)، والتي تكون إما null
أو تشير إلى كائن آخر. يسمى هذا الكائن "النموذج الأولي":
عندما نقرأ خاصية من object
، وتكون مفقودة، تأخذها JavaScript تلقائيًا من النموذج الأولي. في البرمجة، هذا ما يسمى "الميراث النموذجي". وسندرس قريبًا العديد من الأمثلة على هذا الوراثة، بالإضافة إلى الميزات اللغوية الرائعة المبنية عليها.
الخاصية [[Prototype]]
هي خاصية داخلية ومخفية، ولكن هناك طرق عديدة لتعيينها.
أحدها هو استخدام الاسم الخاص __proto__
، مثل هذا:
دع الحيوان = { يأكل: صحيح }; دع الأرنب = { القفزات: صحيح }; أرنب.__proto__ = حيوان; // مجموعات الأرنب.[[النموذج الأولي]] = الحيوان
الآن، إذا قرأنا خاصية من rabbit
، وكانت مفقودة، فستأخذها JavaScript تلقائيًا من animal
.
على سبيل المثال:
دع الحيوان = { يأكل: صحيح }; دع الأرنب = { القفزات: صحيح }; أرنب.__proto__ = حيوان; // (*) // يمكننا العثور على كلا الخاصيتين في الأرنب الآن: تنبيه (أرنب يأكل) ؛ // حقيقي (**) تنبيه (أرنب. قفزات)؛ // حقيقي
هنا السطر (*)
يجعل animal
هو النموذج الأولي rabbit
.
بعد ذلك، عندما يحاول alert
قراءة الخاصية rabbit.eats
(**)
، فهي ليست موجودة في rabbit
، لذا تتبع JavaScript مرجع [[Prototype]]
وتجدها في animal
(انظر من الأسفل إلى الأعلى):
وهنا يمكننا أن نقول أن " animal
هو النموذج الأولي rabbit
" أو " rabbit
يرث النموذج الأولي من animal
".
لذا، إذا كان لدى animal
الكثير من الخصائص والطرق المفيدة، فإنها تصبح متاحة تلقائيًا في rabbit
. تسمى هذه الخصائص "موروثة".
إذا كان لدينا طريقة في animal
، فيمكن أن نسميها على rabbit
:
دع الحيوان = { يأكل: صحيح، يمشي() { تنبيه ("مشية الحيوان")؛ } }; دع الأرنب = { القفزات: صحيح، __بروتو__: حيوان }; // المشي مأخوذ من النموذج الأولي Rabbit.walk(); // المشي الحيوان
يتم أخذ الطريقة تلقائيًا من النموذج الأولي، مثل هذا:
يمكن أن تكون سلسلة النموذج الأولي أطول:
دع الحيوان = { يأكل: صحيح، يمشي() { تنبيه ("مشية الحيوان")؛ } }; دع الأرنب = { القفزات: صحيح، __بروتو__: حيوان }; دع LongEar = { طول الأذن: 10، __بروتو__: أرنب }; // walk مأخوذ من سلسلة النموذج الأولي longEar.walk(); // المشي الحيوان تنبيه (longEar.jumps)؛ // صحيح (من الأرنب)
الآن، إذا قرأنا شيئًا ما من longEar
، وكان مفقودًا، فستبحث JavaScript عنه في rabbit
، ثم في animal
.
لا يوجد سوى اثنين من القيود:
لا يمكن للمراجع أن تدور في دوائر. سوف يلقي جافا سكريبت خطأ إذا حاولنا تعيين __proto__
في دائرة.
يمكن أن تكون قيمة __proto__
إما كائنًا أو null
. يتم تجاهل الأنواع الأخرى.
قد يكون الأمر واضحًا أيضًا، ولكن لا يزال: يمكن أن يكون هناك [[Prototype]]
واحد فقط. لا يجوز لكائن أن يرث من اثنين آخرين.
__proto__
عبارة عن حرف/أداة ضبط تاريخية لـ [[Prototype]]
إنه خطأ شائع للمطورين المبتدئين ألا يعرفوا الفرق بين هذين الاثنين.
يرجى ملاحظة أن __proto__
ليس مثل خاصية [[Prototype]]
الداخلية. إنه مُحضر/أداة ضبط لـ [[Prototype]]
. سنرى لاحقًا المواقف التي يكون فيها الأمر مهمًا، والآن دعونا نضع ذلك في الاعتبار بينما نبني فهمنا للغة JavaScript.
الخاصية __proto__
قديمة بعض الشيء. إنه موجود لأسباب تاريخية، وتقترح جافا سكريبت الحديثة أنه يجب علينا استخدام وظائف Object.getPrototypeOf/Object.setPrototypeOf
بدلاً من الحصول على/تعيين النموذج الأولي. سنقوم أيضًا بتغطية هذه الوظائف لاحقًا.
وفقًا للمواصفات، يجب أن يكون __proto__
مدعومًا فقط من خلال المتصفحات. في الواقع، جميع البيئات بما في ذلك الدعم من جانب الخادم __proto__
، لذلك نحن آمنون تمامًا عند استخدامه.
نظرًا لأن تدوين __proto__
أكثر وضوحًا، فإننا نستخدمه في الأمثلة.
يتم استخدام النموذج الأولي فقط لقراءة الخصائص.
تعمل عمليات الكتابة/الحذف مباشرة مع الكائن.
في المثال أدناه، قمنا بتعيين أسلوب walk
الخاص به rabbit
:
دع الحيوان = { يأكل: صحيح، يمشي() { /* لن يستخدم الأرنب هذه الطريقة */ } }; دع الأرنب = { __بروتو__: حيوان }; أرنب. المشي = وظيفة () { تنبيه("أرنب! ترتد ترتد!"); }; Rabbit.walk(); // أرنب! ترتد ترتد!
من الآن فصاعدًا، يعثر استدعاء rabbit.walk()
على التابع فورًا في الكائن وينفذه، دون استخدام النموذج الأولي:
تعتبر خصائص الوصول استثناءً، حيث تتم معالجة التعيين بواسطة وظيفة الضبط. لذا فإن الكتابة إلى مثل هذه الخاصية هي في الواقع نفس استدعاء دالة.
لهذا السبب يعمل admin.fullName
بشكل صحيح في الكود أدناه:
السماح للمستخدم = { الاسم: "جون"، اللقب: "سميث"، تعيين الاسم الكامل (القيمة) { [this.name, this.surname] = value.split(" "); }, الحصول على الاسم الكامل () { إرجاع `${this.name} ${this.surname}`; } }; دع المشرف = { __بروتو__: المستخدم، المشرف: صحيح }; تنبيه (admin.fullName)؛ // جون سميث (*) // مشغلات واضعة! admin.fullName = "أليس كوبر"; // (**) تنبيه (admin.fullName)؛ // أليس كوبر، تم تعديل حالة المشرف تنبيه (user.fullName)؛ // جون سميث، حالة المستخدم محمية
هنا في السطر (*)
تحتوي الخاصية admin.fullName
على حرف getter في النموذج الأولي user
، لذلك يتم استدعاؤها. وفي السطر (**)
تحتوي الخاصية على أداة ضبط في النموذج الأولي، لذلك يتم استدعاؤها.
قد يظهر سؤال مثير للاهتمام في المثال أعلاه: ما قيمة this
set fullName(value)
؟ أين يتم كتابة الخاصيتين this.name
و this.surname
: في user
أو admin
؟
الجواب بسيط: this
لا يتأثر بالنماذج الأولية على الإطلاق.
بغض النظر عن مكان العثور على الطريقة: في كائن أو نموذجه الأولي. في استدعاء الأسلوب، يكون this
دائمًا هو الكائن الموجود قبل النقطة.
لذا، فإن استدعاء الضبط admin.fullName=
يستخدم admin
كـ this
، وليس user
.
وهذا في الواقع أمر بالغ الأهمية، لأنه قد يكون لدينا كائن كبير به العديد من الأساليب، ولدينا كائنات ترث منه. وعندما تقوم الكائنات الموروثة بتشغيل الأساليب الموروثة، فإنها ستقوم فقط بتعديل حالاتها الخاصة، وليس حالة الكائن الكبير.
على سبيل المثال، يمثل animal
هنا "طريقة تخزين"، ويستفيد منه rabbit
.
يقوم استدعاء rabbit.sleep()
بتعيين this.isSleeping
على كائن rabbit
:
// الحيوان لديه طرق دع الحيوان = { يمشي() { إذا (! this.isSleeping) { تنبيه ("أنا أمشي")؛ } }, ينام() { this.isSleeping = true; } }; دع الأرنب = { الاسم: "الأرنب الأبيض"، __بروتو__: حيوان }; // يعدل Rabbit.isSleeping Rabbit.sleep(); تنبيه (rabbit.isSleeping)؛ // حقيقي تنبيه (animal.isSleeping)؛ // غير محدد (لا يوجد مثل هذه الخاصية في النموذج الأولي)
الصورة الناتجة:
إذا كانت لدينا أشياء أخرى، مثل bird
snake
وما إلى ذلك، ترث من animal
، فإنها أيضًا ستكتسب الوصول إلى الأساليب animal
. لكن this
في كل استدعاء للأسلوب سيكون هو الكائن المقابل، الذي يتم تقييمه في وقت الاتصال (قبل النقطة)، وليس animal
. لذلك عندما نكتب البيانات في this
، يتم تخزينها في هذه الكائنات.
ونتيجة لذلك، تتم مشاركة الأساليب، ولكن حالة الكائن ليست كذلك.
تتكرر حلقة for..in
على الخصائص الموروثة أيضًا.
على سبيل المثال:
دع الحيوان = { يأكل: صحيح }; دع الأرنب = { القفزات: صحيح، __بروتو__: حيوان }; // Object.keys يُرجع المفاتيح الخاصة فقط تنبيه(Object.keys(أرنب)); // يقفز // for..in حلقات على المفاتيح الخاصة والموروثة for(دع الدعامة في الأرنب) تنبيه(دعامة); // يقفز ثم يأكل
إذا لم يكن هذا ما نريده، ونرغب في استبعاد الخصائص الموروثة، فهناك طريقة مدمجة obj.hasOwnProperty(key): تُرجع true
إذا كان obj
له خاصيته الخاصة (غير الموروثة) المسماة key
.
حتى نتمكن من تصفية الخصائص الموروثة (أو القيام بشيء آخر بها):
دع الحيوان = { يأكل: صحيح }; دع الأرنب = { القفزات: صحيح، __بروتو__: حيوان }; ل(دع الدعامة في الأرنب) { Let isOwn = Rabbit.hasOwnProperty(prop); إذا (ملكي) { تنبيه("لدينا: ${prop}`); // لدينا: يقفز } آخر { تنبيه("موروث: ${prop}`); // موروث: يأكل } }
لدينا هنا سلسلة الوراثة التالية: يرث rabbit
من animal
، ويرث من Object.prototype
(لأن animal
كائن حرفي {...}
، لذا فهو افتراضيًا)، ثم فوقه null
:
ملاحظة، هناك شيء واحد مضحك. من أين تأتي طريقة rabbit.hasOwnProperty
؟ لم نحدده. بالنظر إلى السلسلة يمكننا أن نرى أن الطريقة مقدمة من Object.prototype.hasOwnProperty
. بمعنى آخر، إنها موروثة.
…ولكن لماذا لا تظهر hasOwnProperty
في حلقة for..in
مثل eats
و jumps
، إذا كانت for..in
قوائم الخصائص الموروثة؟
الجواب بسيط: إنه غير قابل للتعداد. تمامًا مثل جميع خصائص Object.prototype
الأخرى، فهو يحتوي على علامة enumerable:false
. و for..in
يسرد فقط خصائص لا حصر لها. ولهذا السبب لم يتم إدراجه وبقية خصائص Object.prototype
.
تتجاهل جميع طرق الحصول على المفاتيح/القيمة الأخرى تقريبًا الخصائص الموروثة
تتجاهل جميع طرق الحصول على المفاتيح/القيمة الأخرى تقريبًا، مثل Object.keys
و Object.values
وما إلى ذلك، الخصائص الموروثة.
أنها تعمل فقط على الكائن نفسه. لا تؤخذ الخصائص من النموذج الأولي بعين الاعتبار.
في JavaScript، تحتوي جميع الكائنات على خاصية [[Prototype]]
مخفية والتي تكون إما كائنًا آخر أو null
.
يمكننا استخدام obj.__proto__
للوصول إليه (توجد طرق أخرى سيتم تغطيتها قريبًا).
الكائن المشار إليه بواسطة [[Prototype]]
يسمى "النموذج الأولي".
إذا أردنا قراءة خاصية obj
أو استدعاء طريقة ما، وهي غير موجودة، فستحاول JavaScript العثور عليها في النموذج الأولي.
تعمل عمليات الكتابة/الحذف مباشرة على الكائن، ولا تستخدم النموذج الأولي (بافتراض أنها خاصية بيانات وليست أداة ضبط).
إذا قمنا باستدعاء obj.method()
، وتم أخذ method
من النموذج الأولي، فسيظل this
يشير إلى obj
. لذلك تعمل الأساليب دائمًا مع الكائن الحالي حتى لو كانت موروثة.
تتكرر حلقة for..in
على خصائصها الخاصة والموروثة. جميع طرق الحصول على المفاتيح/القيمة الأخرى تعمل فقط على الكائن نفسه.
الأهمية: 5
هذا هو الكود الذي يقوم بإنشاء زوج من الكائنات، ثم تعديلها.
ما هي القيم التي تظهر في العملية؟
دع الحيوان = { القفزات: فارغة }; دع الأرنب = { __بروتو__: حيوان، القفزات: صحيح }; تنبيه (أرنب. قفزات)؛ // ؟ (1) حذف Rabbit.Jumps؛ تنبيه (أرنب. قفزات)؛ // ؟ (2) حذف Animal.jumps; تنبيه (أرنب. قفزات)؛ // ؟ (3)
يجب أن يكون هناك 3 إجابات.
true
، مأخوذ من rabbit
.
null
، مأخوذة من animal
.
undefined
، لم يعد هناك مثل هذه الخاصية.
الأهمية: 5
المهمة لها جزأين.
نظرا للكائنات التالية:
دع الرأس = { النظارات: 1 }; دع الجدول = { القلم: 3 }; دع السرير = { الورقة: 1، وسادة: 2 }; دع الجيوب = { المال: 2000 };
استخدم __proto__
لتعيين النماذج الأولية بطريقة تجعل أي بحث عن الممتلكات يتبع المسار: pockets
→ bed
→ table
→ head
. على سبيل المثال، يجب أن تكون قيمة pockets.pen
3
(موجود في table
)، ويجب أن تكون قيمة bed.glasses
1
(موجود في head
).
أجب عن السؤال: هل من الأسرع الحصول على glasses
pockets.glasses
أم head.glasses
؟ المعيار إذا لزم الأمر.
دعنا نضيف __proto__
:
دع الرأس = { النظارات: 1 }; دع الجدول = { القلم: 3، __بروتو__: الرأس }; دع السرير = { الورقة: 1، وسادة: 2، __بروتو__: الجدول }; دع الجيوب = { المال : 2000 __بروتو__: السرير }; تنبيه(جيوب.قلم); // 3 تنبيه(bed.glasses); // 1 تنبيه(table.money); // غير محدد
في المحركات الحديثة، من حيث الأداء، ليس هناك فرق بين أخذ خاصية من جسم ما أو من نموذجه الأولي. يتذكرون مكان العثور على العقار ويعيدون استخدامه في الطلب التالي.
على سبيل المثال، بالنسبة إلى pockets.glasses
، يتذكرون المكان الذي وجدوا فيه glasses
(في head
)، وفي المرة القادمة سيبحثون هناك. كما أنها ذكية بما يكفي لتحديث ذاكرة التخزين المؤقت الداخلية إذا تغير شيء ما، بحيث يكون التحسين آمنًا.
الأهمية: 5
لدينا rabbit
يرث من animal
.
إذا اتصلنا بـ rabbit.eat()
، فما هو الكائن الذي يحصل على الخاصية full
: animal
أم rabbit
؟
دع الحيوان = { يأكل() { this.full = true; } }; دع الأرنب = { __بروتو__: حيوان }; أرنب.أكل();
الجواب : rabbit
.
ذلك لأن this
كائن قبل النقطة، لذا rabbit.eat()
يعدل rabbit
.
البحث عن الممتلكات وتنفيذها شيئان مختلفان.
تم العثور على الطريقة rabbit.eat
لأول مرة في النموذج الأولي، ثم تم تنفيذها باستخدام this=rabbit
.
الأهمية: 5
لدينا نوعان من الهامستر: speedy
lazy
يرثان من كائن hamster
العام.
وعندما نطعم أحدهما، يكون الآخر ممتلئًا أيضًا. لماذا؟ كيف يمكننا اصلاحها؟
دع الهامستر = { معدة: []، أكل (الطعام) { this.stomach.push(food); } }; دعونا سريع = { __بروتو__: الهامستر }; دع كسول = { __بروتو__: الهامستر }; // هذا وجد الطعام speedy.eat("أبل"); تنبيه (سريع. المعدة)؛ // تفاحة // هذا أيضًا موجود، لماذا؟ الإصلاح من فضلك. تنبيه(lazy.stomach); // تفاحة
دعونا ننظر بعناية إلى ما يحدث في المكالمة speedy.eat("apple")
.
تم العثور على الطريقة speedy.eat
في النموذج الأولي ( =hamster
)، ثم تم تنفيذها باستخدام this=speedy
(الكائن الموجود قبل النقطة).
بعد ذلك، يحتاج this.stomach.push()
إلى العثور على خاصية stomach
واستدعاء الدالة push
عليها. يبحث عن stomach
في this
( =speedy
)، ولكن لم يتم العثور على شيء.
ثم يتبع سلسلة النموذج الأولي ويجد stomach
في hamster
.
ثم يدعو إلى push
عليه، مما يضيف الطعام إلى معدة النموذج الأولي .
لذا فإن جميع الهامستر يتشاركون في معدة واحدة!
لكل من lazy.stomach.push(...)
و speedy.stomach.push()
، يتم العثور على stomach
الخاصية في النموذج الأولي (لأنها ليست في الكائن نفسه)، ثم يتم دفع البيانات الجديدة إليه.
يرجى ملاحظة أن مثل هذا الشيء لا يحدث في حالة مهمة بسيطة this.stomach=
:
دع الهامستر = { معدة: []، أكل (الطعام) { // عيّن إلى this.stomach بدلاً من this.stomach.push this.stomach = [food]; } }; دعونا سريع = { __بروتو__: الهامستر }; دع كسول = { __بروتو__: الهامستر }; // سريعًا وجد الطعام speedy.eat("أبل"); تنبيه (سريع. المعدة)؛ // تفاحة // معدة الكسول فارغة تنبيه(lazy.stomach); // <لا شيء>
الآن كل شيء يعمل بشكل جيد، لأن this.stomach=
لا يقوم بإجراء بحث عن stomach
. تتم كتابة القيمة مباشرة في this
الكائن.
كما يمكننا تجنب المشكلة تمامًا عن طريق التأكد من أن كل هامستر لديه معدته الخاصة:
دع الهامستر = { معدة: []، أكل (الطعام) { this.stomach.push(food); } }; دعونا سريع = { __بروتو__: الهامستر, معدة: [] }; دع كسول = { __بروتو__: الهامستر, معدة: [] }; // سريعًا وجد الطعام speedy.eat("أبل"); تنبيه (سريع. المعدة)؛ // تفاحة // معدة الكسول فارغة تنبيه(lazy.stomach); // <لا شيء>
كحل شائع، يجب كتابة جميع الخصائص التي تصف حالة جسم معين، مثل stomach
أعلاه، في هذا الكائن. وهذا يمنع مثل هذه المشاكل.