مفهوم الـ Prototype في جافاسكربت
- مفهوم النموذج الأولي في جافاسكربت
- فوائد النماذج الأولية في جافاسكربت
- أمثلة عملية على الـنموذج الأولي في جافاسكربت
- مفهوم Prototype Chain في جافاسكربت
- الخاصية
__proto__في جافاسكربت - الكلاس و النموذج الأولي في جافاسكربت
مفهوم النموذج الأولي في جافاسكربت
جافاسكربت هي لغة تعتمد على النموذج الأولي (Prototype Based)، أي أن الوراثة فيها تتم من خلال قوالب (Prototypes) و ليس عبر كلاسات (Classes) كما في لغات البرمجة الأخرى.
كل كائن في جافاسكربت يحتوي على نموذج أولي يضم الخصائص و الدوال التي يمكن أن يرثها منه أي كائن آخر، مما يُنشئ سلسلة من الوراثة تُعرف باسم سلسلة النموذج الأولي (Prototype Chain).
فوائد النماذج الأولية في جافاسكربت
1. الوراثة ومشاركة التعليمات البرمجية (Code Reusability)
النموذج الأولي هو طريقة جافاسكربت لتطبيق الوراثة (Prototypal Inheritance).
- مبدأ (DRY) يتيح لك النموذج الأولي تحديد دالة (Function) أو خاصية (Property) مرة واحدة فقط على النموذج الأولي للدالة المُنشئة (Constructor Function).
- وصول الجميع جميع الكائنات التي يتم إنشاؤها من هذه الدالة المُنشئة ترث هذه الدالة أو الخاصية و تتمكن من الوصول إليها عبر سلسلة النموذج الأولي (Prototype Chain).
- مثال عملي بدلاً من إضافة دالة
greet()لكل مستخدم جديد على هذا النحوuser1.greet = function() {...}وuser2.greet = function() {...}فإنه يتم تعريفها مرة واحدة هكذاUser.prototypeوتكون متاحة لكل منuser1وuser2و غيرهما.
2. كفاءة الذاكرة (Memory Efficiency)
هذه هي أكبر فائدة عملية للنموذج الأولي.
- تجنب الازدواجية عندما تضيف دالة إلى النموذج الأولي، يتم تخزين هذه الدالة في الذاكرة مرة واحدة فقط.
- توفير الموارد بغض النظر عن عدد الكائنات (النسخ) التي تنشئها من الدالة المُنشئة، فإنها جميعها تشير إلى نفس الدالة المخزنة على النموذج الأولي. هذا يوفر مساحة كبيرة من الذاكرة مقارنة بإنشاء نسخة جديدة من الدالة لكل كائن.
3. المرونة و التعديل الديناميكي (Dynamic Modification)
تسمح النماذج الأولية بمرونة عالية في تعديل سلوك الكائنات في وقت التشغيل (Runtime).
- إضافة لاحقة يمكنك إضافة خصائص أو دوال جديدة إلى النموذج الأولي لدالة مُنشئة حتى بعد إنشاء الكائنات منها.
- تأثير فوري ستكتسب جميع الكائنات الموجودة و الجديدة تلقائياً الوظيفة الجديدة بمجرد إضافتها إلى النموذج الأولي.
4. أساس البرمجة الكائنية التوجه (OOP Foundation)
تُشكل النماذج الأولية الأساس الذي بُنيت عليه ميزات الكلاسات ( Classes ) في إصدارات ECMAScript الحديثة سواء ES6 و الإصدارات الأحدث.
على الرغم من أن جافاسكربت تستخدم الآن الكلاس بشكل مشابه للغات البرمجة الأخرى، إلا أن الكلاس فيها ما هو إلا هو أسلوب مبني فوق نظام النماذج الأولية القديم، مما يعني أن فهم النماذج الأولية يظل أساسياً لفهم كيفية عمل الوراثة في جافاسكربت بشكل حقيقي.
أمثلة عملية على الـنموذج الأولي في جافاسكربت
أمثلة حول تحسين استهلاك الذاكرة
الفكرة من المثال التالي هي أن لا نكرر الدوال في كائن حيث قمنا ببناء دالة تمثّل كائن إسمه Car يوجد فيه خصائص هي name و color بالإضافة إلى دالة إسمها drive.
المثال الأول
function Car(name, color) { this.name = name; this.color = color; this.drive = function() { console.log("Driving"); } }; let car1 = new Car("Mersedes", "Black"); let car2 = new Car("BMW", "Red");
في مثالنا السابق، في كل مرة يتم فيها استخدام الأمر new Car() فإنه سيتم إنشاء نسخة له في الذاكرة من الدالة drive و هذا يسبب إهدار في الذاكرة.
الآن سيأتي دور النموذج الأولي لحل مشكلة إهدار الذاكرة حيث أنه يتم تعريف الدالة فيه مرة واحدة فقط.
في المثال التالي، أي نسخة يتم إنشاؤها من Car سيكون فيه الدالة drive نفسها.
المثال الثاني
function Car(name, color) { this.name = name; this.color = color; }; Car.prototype.drive = function() { console.log("Driving"); } let car1 = new Car("Mersedes", "Black"); let car2 = new Car("BMW", "Red");
في مثالنا السابق، بدلاً من أن نكتب الدالة drive في كل كائن، قمنا بوضع الدالة في النموذج الأولي Car.prototype الذي يعتبر بمثابة مكان مشترك لكل الكائنات، التي نوعها Car و بذلك نوفّر مساحة من الذاكرة.
أمثلة حول تغيير سلوك الدوال وقت التشغيل
في جافاسكربت تستطيع وقت التشغيل أن تغير سلوك دالة معينة حسب منطق معيّن أو عند تحقق شرط معين.
في المثال التالي، نلاحظ أن الدالة نفسها المعرّفة في الذاكرة يتغير سلوكها بالكامل حسب تحقق شرط معين.
المثال الأول
// هنا تم تعريف دالة تمثل كائن مع وضع خاصية الإسم له function Person(name) { this.name = name; } // Person إلى النموذج الأولي للنوع doSomething هنا تم إضافة دالة إسمها Person.prototype.doSomething = function() { console.log("Walking..."); } // doSomething() و بعدها تم استدعاء الدالة Person هنا تم إنشاء كائن من let person = new Person("Waleed"); person.doSomething(); // "Walking ..." هنا سيتم طباعة الجملة // هنا وضعنا شرط يتحقق لا مجال، أي أنه سيتم تنفيذ الأوامر الموضوعة فيه // doSomething() و هذا يعني أنه ستتغير الجملة التي سيتم طباعتها عند استدعاء الدالة if (true) { Person.prototype.doSomething = function () { console.log("Running..."); } } person.doSomething(); // "Running ..." هنا سيتم طباعة الجملة
النتيجة
Running ...
في المثال التالي قمنا بتغيير منطق الدالة toString() الموجودة في الكائن String في وقت التشغيل.
المثال الثاني
// هنا تم تعريف متغير نصي let str = "Hello world"; // لجعلها تطبع رسالة ترحيب String الموجودة في النوع toString() هنا تم تغيير النموذج الأولي للدالة String.prototype.toString = function () { console.log("Hello I am toString function ..."); } // str من الكائن toString() هنا تم استدعاء الدالة str.toString(); // "Hello I am toString function ..." هنا سيتم طباعة الجملة
النتيجة
Hello I am toString function ...
في المشاريع الحقيقية، لا تقم بتغيير منطق كود الدالة toString() كما فعلنا في المثال السابق لأن ذلك قد يؤدي لمشاكل غير متوقعة.
لا ينصح بكتابة منطق كود الدالة toString() كما فعلنا في المثال السابق إلا في حالات خاصة و معيّنة للأسباب التالية:
- عند تغيير دالة
toString()كما في المثال السابق فإن الدالة ستتغير أيضاً بالنسبة لأي كائن String لأنه عند تغيير الدالة في النموذج الأولي فإن ذلك سيؤثر على كل كائنات التي نوعهاString. - قد تستخدم مكتبة تستخدم هذه الدالة التي غيرتها وبالتالي النتائج ستكون غير متوقعه او ستضهر اخطاء.
- صعوبة في ال Debugging .فمثلا لو كان مبرمج اخر يستخدم كودك وانت غيرت منطق دالة وقت التشغيل قد يكون هناك صعوبة بتتبع الكود لانه قد يتصرف بطريقة غريبة وغير ما هو متوقع من المبرمج.
و لكن هذا السلوك مفيد في الحالات التالية:
- عند إضافة دالة ليست موجودة في جافاسكربت.
فمثلاً، في السابق لم يكن هناك دالة إسمهاincludes()في جافاسكربت فكان المطورون يضيفوا هذه الدالة على النحو التالي.if (!Array.prototype.includes) { Array.prototype.includes = function(item) { return this.indexOf(item) !== -1; }; }
- عند بناء مكتبة خاصة أو إطار عمل خاص بك (أي مشروع مغلق أو مكتبة مغلقة و ليست عامة).
- لأغراض تجريبية موقتة لترى نتيجة معينة (أي لتتبع الأخطاء).
- لاختبار الاختراق، مثلاً تريد تغيير سلوك دالة معينة وقت التشغيل لتجاوز نظام حماية معينة. يكون هذا النظام يفعل فلترة لدالة معينة كي لا تعمل فأنت تقوم بتغيير دالة أخرى حتى تصبح تؤدي نفس الوظيفة، كيف ترا على سبيل المثال إن كنت تستطيع تجاوز حماية هذا النظام أم لا.
مفهوم Prototype Chain في جافاسكربت
عندما تستدعي دالة من كائن معين مثل car.drive() ستبحث جافاسكربت عن الدالة drive() أولاً في الكائن car() نفسه. إن لم تجده ستبدأ بالبحث في سلسلة النماذج (Prototype Chain)، أي أنها ستبحث في car.prototype و ان لم تجده ستبحث في الكائن الأب له و الذي هو Object.prototype .
جميع الكائنات في جافاسكربت ترث من Object، فهو بمثابة أب لجميع الكائنات (أي أنه لا يرث من أحد).
سلسلة النماذج (Prototype Chain)، هي السبب في أن أي كائن في جافاسكربت يملك دوال جاهزة مثل toString() و hasOwnProperty() حتى لو لم نعرفها بأنفسنا.
الخاصية __proto__ في جافاسكربت
كل كائن في جافاسكربت لديه خاصية خفية إسمها __proto__ تشير إلى النموذج الأوّلي الذي أتى منه الكائن.
في المثال التالي أنشأنا كائن من String إسمه str و بعدها قمنا بالتحقق ما إن كانت الخاصية __proto__ تشير إلى أنه نموذج نوعه String أم لا.
مثال
let str = "harmash"; console.log(str.__proto__ === String.prototype)
النتيجة
true
الآن في حال قمت بطباعة __proto__ فإنه سيظهر لك معلومات حول كما يحتويه.
مثال
let str = "harmash"; console.log(str.__proto__)
النتيجة
String {'', anchor: ƒ, at: ƒ, big: ƒ, blink: ƒ, …} anchor : ƒ anchor() at : ƒ at() big : ƒ big() blink : ƒ blink() bold : ƒ bold() charAt : ƒ charAt() charCodeAt : ƒ charCodeAt() codePointAt : ƒ codePointAt() concat : ƒ concat() constructor : ƒ String() endsWith : ƒ endsWith() ...
الكلاس و النموذج الأولي في جافاسكربت
إبتداءاً من الإصدار ES6 من لغة جافاسكربت و الذي صدر عام 2015، فإنه تم إضافة طريقة جديدة لتحديد الشكل العام للكائن و هي باستعمال الكلمة المفتاحية class و لكن فعلياً ما هي إلا مجرّد أسلوب لتسهيل كتابة الكود ( Syntactic Sugar ) على المبرمجين الذين يتعاملون مع لغات برمجة أخرى. بمعنى أنه خلف الكواليس ما زالت و ستبقى جافاسكربت تعمل بأسلوب النماذج ( Prototype Based ).
إذاً عند تعريف كلاس على النحو التالي.
مثال
class Person { sayHello() {} }
فإن جافاسكربت تتعامل معه كنموذج أولي كما يلي.
مثال
Person.prototype.sayHello = function() {}
