Prototypes - אבי טיפוס, זה המנגנון שבאמצעותו אובייקטים של javascript יורשים מאפיינים אחד מהשני. במאמר זה אנחנו נסביר כיצד שרשראות אבי טיפוס - (prototype chains) עובדות ונסתכל כיצד מאפיין (property) בשם prototype יכול לשמש עבור הוספת מתודות ל-constructors קיימים.
ידע מוקדם: |
הבנה של פונקציות ב-JavaScript, הכרות עם הבסיס של JavaScript (ראו את המאמר צעדים ראשונים ב-צעדים ראשונים ב-JavaScript ו-אבני הבניין של JavaScript) וכן הבסיס של תכנות מונחה עצמית ב-JavaScript (ראו אובייקטים - עקרונות יסוד). |
---|---|
מטרה: | הבנה של JavaScript object prototypes, כיצד שרשראות prototype עובדות, וכיצד להוסיף מתודות חדשות לתוך פרופ׳ בשם prototype . |
שפה מבוססת אב-טיפוס?
JavaScript לעיתים קרובות מתוארת כשפה מונחית אבי-טיפוס (prototype-based language) - על מנת לאפשר הורשה, לאובייקטים יכול להיות אובייקט אב-טיפוס (prototype object), אשר מתפקד כאובייקט תבנית - אובייקט אב טיפוס אשר ממנו האובייקטים יכולים לירוש מתודות (methods) ומאפיינים (properties).
לאובייקט אב-הטיפוס (prototype), יכול להיות גם אובייקט אב-טיפוס משל עצמו, אשר ממנו הוא יורש מתודות (methods) ומאפיינים (properties) וכך הלאה. תהליך זה לרוב מוגדר כשרשרת אבי הטיפוס (prototype chain), אשר מסבירה מדוע לאובייקטים שונים יש מאפיינים ומתודות שזמינים עבורם, כאשר אלו בכלל הוגדרו באובייקטים אחרים.
ליתר דיוק, המתודות והמאפיינים מוגדרים במאפיין (property) בשם prototype
ב-constructor functions ולא באובייקטים עצמם שנוצרו (object instance).
ב-JavaScript, נוצר קשר בין האובייקט שנוצר (object instance) לבין אב הטיפוס/prototype שלו באמצעות ״הליכה״ על שרשרת אבי הטיפוס. אנו נוכל לראות את אב הטיפוס של אובייקט ב-property של האובייקט שנוצר בשם __proto__
. שם זה נגזר מ-prototype
שמוגדר בכלל ב-constructor.
לתשומת לב: חשוב להבין שיש אבחנה בין אב הטיפוס של האובייקט (object' s prototype) אשר זמין באמצעות (Object.getPrototypeOf(obj
או באמצעות מאפיין __proto__
לבין מאפיין (property) בשם אב-טיפוס (prototype) אשר נמצא ב-constructor functions.
הראשון הוא מאפיין שיופיע על כל אובייקט שייווצר ויעיד מיהו אב הטיפוס של אותו אובייקט ואילו השני, אשר נמצא ב-constructor הוא בעצם מאפיין של אותו constructor.
נסתכל על דוגמא על מנת להבין זאת לעומק.
הבנה של אובייקטים מסוג אב-טיפוס
נחזור לדוגמא הקודמת שבה סיימנו לכתוב את ה-constructor שלנו ()Person
:
העלו את הדוגמא בדפדפן שלכם. אם אין לכם את הדוגמא עצמה או שהיא אינה עובדת, אנא השתמשו בדוגמא שלנו אשר נמצאת בקישור זה או בקוד המקור.
בדוגמא זו, אנחנו מגדירים את ה -constructor function שלנו כך:
function Person(first, last, age, gender, interests) {
// property and method definitions
this.first = first;
this.last = last;
//...
}
אנו יוצרים אובייקטים מה-constructor function שהגדרנו כך:
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
אם תקלידו .person1
בקונסולה, אתם אמורים לראות שהדפדפן מנסה להשלים באופן אוטומטי אפשרויות הזמינות עבור אובייקט זה:
ברשימה זו, אתם יכולים לראות את הפרופ׳ ומתודות שהוגדרו ב-constructor Person()
— name
, age
, gender
, interests
, bio
, ו- greeting
. בנוסף, אנחנו גם נראה פרופ׳ ומתודות נוספות - watch
, valueOf
וכד׳ - אלו מוגדרים בתבנית אב הטיפוס (prototype object) של ()Person
, שזה Object
.
מה קורה כשאנחנו רוצים לקרוא למתודה, להפעיל מתודה על person1
, אשר מוגדרת ב-Object
?:
person1.valueOf()
המתודה הזו - ()Object.valueOf
נורשת על ידי person1
בגלל שתבנית אב הטיפוס של האובייקט (הפונקציה ()Person
) שיצר אותו היא ()Object.
המתודה ()valueOf
מחזירה את הערך של האובייקט שהיא נקראה עליו:
- הדפדפן תחילה בודק לראות האם לאובייקט
person1
יש את המתודה()valueOf
זמינה עליו, כפי שמוגדר ב-constructor שלו,()Person
. - אם לא - הוא בודק האם לאב הטיפוס של
()Person
, יש את המתודה()valueOf
זמינה עליו. יש לו ולכן היא זמינה עבורperson1
.
לתשומת לב: אנחנו רוצים להדגיש את העבודה שמתודות ופרופ׳ לא מועתקים מאובייקט אחד לשני ב-prototype chain - הם זמינים עבורם באצעות הליכה על prototype chain כפי שהסברנו למעלה..
לתשומת לב: אין דרך רשמית להיכנס לאובייקט אב הטיפוס של אובייקט בצורה ישירה הקשרים שבין הפריטים בשרשרת מוגדר בתוך פרופ׳ פנימי - שנקרא [[prototype]]
במסמכים של השפה - ראו גם ECMAScript.
מרבית הדפדפנים המודרניים מכילים פרופ׳ שזמין שנקרא בשם __proto__
, אשר מכיל את האובייקט שהוא תבנית האב של האובייקט הרלוונטי. לדוגמא, נסו להזין person1.__proto__
ו- person1.__proto__.__proto__
לראות איך זה נראה. החל מ-ECMAScript 2015 אנחנו יכולים לגשת לאובייקט אב הטיפוס של אובייקט באמצעות Object.getPrototypeOf(obj)
.
The prototype property: היכן שמגדירים מה מורישים
אז, היכן הפרופ׳ והמתודות שמורישים מוגדרים? אם אנחנו מסתכלים על הדף Object
, אנחנו נראה רשימה מצד של שמאל של פרופ׳ ומתודות - הרבה יותר מאלו שראינו שזמינות עבור person1
. למה? מכיוון שחלקם הורשו ל-person1
וחלקם לא.
כפי שהוסבר למעלה, אלו שנורשו הם אלו שמוגדרים במאפיין (property) בשם prototype
- כלומר אלו הם שמתחילים עם .Object.prototype
ולא אלו שרק מתחילים עם Object
. הערך של המאפיין prototype
הוא אובייקט, אשר בעצם הוא מאחסן את כל הפרופ׳ והמתודות שאנחנו רוצים להוריש לאובייקטים בהמשך ה-prototype chain.
כך לדוגמא, ()Object.prototype.watch
ו- ()Object.prototype.valueOf
זמינות עבור כל אובייקט שיירש מ-Object.prototype
, כולל מופעים חדשים של אובייקטים מה-()
constructor Person
.
לעומת זאת, ()Object.is
ו-()Object.keys
לדוגמא, לא מוגדרים בתוך ()
ולכן לא נוריש אותם לאובייקטים אשר יירשו מ-Object.prototype
. הם מתודות ופרופ׳ אשר זמינים רק עבור ה-constructor Object
עצמו.
לתשומת לב: זה נראה מוזר - כיצד יכולה להיות מתודה שמוגדרת על ה-constructor, שהיא בעצמה פונקציה? פונקציות הם גם סוג של אובייקט - ראו הדף בנושא ()Function
להמשך פרטים.
- אתם יכולים לבדוק את הפרופ׳ בשם ה-prototype של האובייקטים הנוכחיים שלנו - חזרו לדוגמא הקודמת והקלידו את הקוד הבא בקונסולה:
Person.prototype
- אתם לא תראו יותר מדי אפשרויות מכיוון שלא הגדרנו כלום בפרופ׳ בשם prototype של ה-constructor. כברירת מחדל, הפרופ׳ בשם prototype של ה-constructor תמיד מתחיל ריק.
- כעת, נסו להזין את הקוד הבא:
Object.prototype
אתם תראו מספר גדול של מתודות שמוגדרות ב-property בשם prototype
של Object
, אשר זמינות עבור אובייקטים שירשו מ-Object
כפי שהסברנו למעלה.
אתם תראו דוגמאות כאלו של שרשרת ההורשה - prototype chain inheritance בכל JavaScript - נסו לחפש אחר מתודות ופרופ׳ אשר מוגדרים בפרופ׳ של האובייקטים הגלובליים String
, Date
, Number
, ו-Array
. לכולם יש מס׳ מתודות ופרופ׳ שמוגדרות באותו prototype. זו הסיבה לדוגמא שכשאר אנחנו יוצרים מחרוזת בצורה הבאה:
var myString = 'This is my string.';
ל-myString
ישר יש מספר מתודות שימושיות שזמינות עבורו כמו ()
split()
, indexOf()
, replace
וכד׳.
לתשומת לב: אנו ממליצים לקרוא את המדריך המעמיק שלנו בנושא - Using prototypes in JavaScript לאחר שהבנתם את החלק הזה. החלק הזה בכוונה נכתב בצורה פשוטה (יחסית) על מנת להסביר את הרעיון שעומד מאחורי הנושא.
חשוב: המאפיין (prototype
(property הוא אחד מהחלקים המבלבלים ב-JavaScript. אתם עלולים לחשוב ש-this מצביע על האובייקט אב טיפוס של האובייקט הנוכחי, אבל הוא לא (זה אובייקט פנימי שניתן לגשת אליו באמצעות __proto__
, זוכרים? ).
המאפיין prototype
הוא בעצם property שמכיל אובייקט, אשר על אובייקט זה אנחנו מגדירים את המתודות והפרופ׳ שנרצו שיורשו.
שימוש במתודה ()create
מוקדם יותר ראינו כיצד מתודת ()Object.create
יכולה לשמש על מנת ליצור אובייקט חדש:
- לדוגמא, נסו להזין בקונסולה של הדוגמא הקודמת את הקוד הבא:
var person2 = Object.create(person1);
- מה ש-
()create
עושה בפועל היא ליצור אובייקט חדש מ-prototype מוגדר. כאןperson2
מיוצר כאובייקט חדש באמצעות שימוש ב-person1
כאובייקט ה-prototype שלו, כאובייקט אב הטיפוס שלו. אתם יכולים לראות זאת באמצעות הזנת הקוד הבא בקונסולה: -
person2.__proto__
זה יחזיר לנו person1.
המאפיין (property) בשם constructor
לכל constructor function יש מאפיין (property) בשם prototype
אשר הערך שלו הוא אובייקט. אותו אובייקט מכיל מאפיין (property) בשם constructor
.
המאפיין constructor
הזה, מצביע ל-constructor function המקורית. כפי שתראו בהמשך, כאשר properties שמוגדרים על Person.prototype property, או באופן כללי, על המאפיין prototype
של constructor function, שהוא בעצמו הוא אובייקט, הם נהיים זמינים עבור כל האובייקטים שייווצרו באמצעות ה-constructor בשם ()Person. המאפיין constructor
זמין גם עבור האובייקט person1
וגם עבור האובייקט person2
.
- לדוגמא, נסו להזין את השורות הבאות בקונסולה:
person1.constructor person2.constructor
שתיהן אמורות להחזיר לנו ה-
Person()
constructor, שכן היא מכילה את ה-״הגדרה״ המקורית של אובייקטים אלו. טריק חכם הוא שניתן לשים סוגריים רגילות()
בסוף המאפייןconstructor
(ובתוך הסוגריים להכניס פרמרטים הנדרשים ל-constructor
, ככל ונדרשים), וואז נוצר לנו אובייקט חדש מאותוconstructor
. ה-constructor
הוא בעצם פונקציה אחרי הכל, אז אפשר לקרוא לפונקציה באמצעות שימוש ב-()
כמו שאנחנו יודעים. רק חשוב לשים את המילה השמורהnew
לפני, על מנת להגדיר שאנחנו רוצים שיווצר אובייקט חדש ולהשתמש בפונקציה הזו כ-constructor
של אותו אובייקט. - נסו להזין את הקוד הבא בקונסולה:
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
- כעת, נסו לגשת למתודות ולפרופ׳ של האובייקט החדש:
person3.name.first person3.age person3.bio()
זה עובד מצויין. אנחנו בדרך לא נשתמש באופציה שכזו, אבל זה יכול להיות שימושי כאשר אנחנו רוצים ליצור מופע אובייקט חדש ואין לנו הפנייה לקונסטרקטור המקורי בצורה פשוטה מכל סיבה שהיא.
ה-property בשם constructor
שימושי גם לדברים נוספים. לדוגמא, אם יש לנו אובייקט ואני רוצה להחזיר את שם ה-constructor
שהוא מופע שלו, כלומר שבנה אותו, אנחנו יכולים להזין את הקוד הבא:
instanceName.constructor.name
נסו לדוגמא להזין את הקוד הבא:
person1.constructor.name
לתשומת לב: הערך של constructor.name
יכול להשתנות (כתוצאה מ-prototypical inheritance, binding, preprocessors, transpilers, etc. ועד), אז לדוגמאות מורכבות יותר, אנחנו נרצה להתשמש באופרטור instanceof
במקום.
שינוי של הפרופ׳ בשם prototype - הוספה/הסרה של מתודות
נסתכל כעת על דוגמא לשינוי של ה-property בשם prototype
שנמצא ב-constructor function - מתודות שיתווספו ל-prototype
יהיו זמינות עבור כל האובייקטים שנוצרו מאותה constructor function. בנקודה הזו אנחנו נוסיף מתודות ל-property בשם prototype
של constructor function שלנו, כך שכל מה שנגדיר במאפיין prototype
יהיה זמין עבור האובייקטים שייווצרו ממנו.
- חזרו לדוגמא המופיע בקישור oojs-class-further-exercises.htm וצרו עותק מקומי של קוד המקור source code. מתחת לקוד הנוכחי של של JavaScript, הוסיפו את הקוד הבא, אשר יוצר מתודה חדשה במאפיין
prototype
של constructor function: -
Person.prototype.farewell = function() { alert(this.name.first + ' has left the building. Bye for now!'); };
- שמרו את הקוד והעלו את הדף בדפדפן, ונסו להזין את הקוד הבא :
person1.farewell();
אתם אמורים לקבל הודעה קופצת מסוג alert, המכילה את שם ה-person שהוגדר ב-constructor. זה מאוד שימוש, אבל מה שיותר שימושי זה שכל שרשרת ההורשה עודכנה באופן דינאמי ואוטומטי והפכה את המתודה הזו לזמינה עבור כל האובייקטים שנוצרו באמצעות אותה constructor function.
חשבו על זה לרגע, בקוד שלנו אנחנו הגדרנו את ה-constructor function, ואז יצרנו אובייקט מאותה constructor function, ואז הוספנו מתודה נוספות ל-prototype
של אותה constructor function:
function Person(first, last, age, gender, interests) {
// property and method definitions
}
var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
};
ומה שקרה זה שהמתודה שהגדרנו ()farewell
נהייתה זמינה עבור person1
- והוא יכול לעשות בה שימוש, למרות שהוא הוגדר לפני שהגדרנו את המתודה החדשה. מגניב, לא?
לתשומת לב: אם אתם נתקלים בבעיות, ראו את הדוגמא פה או כדף אינטרנט.
יחד עם זאת, לעיתים נדירות נראה properties שמוגדרים ב-property הprototype
מכיוון שהם לא ממש גמישים כשהם מוגדרים בצורה הזו. לדוגמא, ניתן להוסיף property בצורה הזו:
Person.prototype.fullName = 'Bob Smith';
זה לא ממש מאפשר גמישות (שכן יכול להיות שקוראים לאובייקט person החדש בשם אחר), ולכן יהיה עדיף לבנות את המאפיין החדש בצורה הבאה:
Person.prototype.fullName = this.name.first + ' ' + this.name.last;
יחד עם זאת, זה לא יעבוד שכן this
יפנה לסקופ הגלובלי במקרה הנוכחי ולא לסקופ של הפונקציה. קריאה למאפיין זה תחזיר ערך של undefined undefined
.
זה עובד מצוין על מתודה שהגדרנו למעלה ב-prototype מכיוון שהיא יושבת בתוך הסקופ של ה-function, והמתודה מועברת בתורה לסקופ של האובייקט שנוצר באמצעות constructor function. אז אולי נגדיר constant properties ב- prototype, כאלו שלא נשנה לעולם, אבל בכללי, זה עובד טוב יותר להגדיר properties בתוך ה-constructor.
בעקרון, השימוש המקובל להגדרת אובייקטים הוא להגדיר את המאפיינים (properties) בתוך ה-constructor ואת המתודות בתוך ה-prototype.
זה הופך הקוד לקל יותר לקריאה, שכן ה-constractor מכיל רק את ההגדרות של properties ואילו המתודות מחולקות לבלוקים נפרדים. לדוגמא:
// Constructor with property definitions
function Test(a, b, c, d) {
// property definitions
}
// First method definition
Test.prototype.x = function() { ... };
// Second method definition
Test.prototype.y = function() { ... };
// etc.
ניתן לראות שימוש בפועל בדוגמא school plan app של Piotr Zalewa.
לסיכום
מאמר זה כיסה את הנושא של JavaScript object prototypes, כולל כיצד prototype object chains מאפשרת לאובייקטים לירוש מתודות ופרופ׳ אחד מהשני, ראינו את ה-property בשם prototype וכיצד הוא יכול לשמש על מנת להוסיף מתודות ל-constructors וכן נושאים נוספים.
במאמר הבא אנחנו נראה כיצד ניתן להחיל הורשה של פונצקיונליות בין שני אובייקטים שניצור.