פרוטוטייפים ומחלקות בג'אווהסקריפט¶
הרבה מתחילים לומדים class בג'אווהסקריפט וחושבים שזו מערכת מחלקות "אמיתית" כמו בשפות אחרות. זו טעות שכדאי לתקן מוקדם - מתחת למכסה המנוע, ג'אווהסקריפט לא באמת עובד עם מחלקות. היא עובדת עם משהו שנקרא פרוטוטייפים (prototypes), ו-class הוא רק שכבת תחביר נוחה מעליהם.
מה זה פרוטוטייפ - ההסבר הבסיסי¶
כל אובייקט בג'אווהסקריפט מחזיק קישור נסתר לאובייקט אחר, שנקרא הפרוטוטייפ שלו. כשמנסים לגשת לתכונה שלא קיימת ישירות על האובייקט, ג'אווהסקריפט לא מוותרת - היא הולכת לבדוק אם התכונה קיימת על הפרוטוטייפ שלו, ואם לא, על הפרוטוטייפ של הפרוטוטייפ, וכך הלאה. השרשרת הזו נקראת שרשרת הפרוטוטייפים (prototype chain).
const animal = {
makeSound() {
console.log("קול כלשהו");
},
};
const dog = Object.create(animal);
dog.makeSound(); // "קול כלשהו" - נמצא בפרוטוטייפ, לא ישירות על dog
dog עצמו לא מכיל את makeSound, אבל כשקוראים ל-dog.makeSound(), ג'אווהסקריפט לא מוצאת את זה על dog, אז היא ממשיכה לחפש בפרוטוטייפ שלו - animal - ומוצאת שם. זו בדיוק המהות של ירושה פרוטוטייפית - שיתוף התנהגות בלי להעתיק אותה לכל אובייקט בנפרד.
פונקציות בנאי - הדרך הישנה יותר¶
לפני שהיה class, יצרו "מחלקות" בעזרת פונקציות רגילות שנקראות עם new:
function Animal(name) {
this.name = name;
}
Animal.prototype.makeSound = function () {
console.log(`${this.name} משמיע קול`);
};
const cat = new Animal("חתול");
cat.makeSound(); // "חתול משמיע קול"
כל פונקציה בג'אווהסקריפט מגיעה עם תכונה בשם prototype, וכל אובייקט שנוצר עם new מקבל את הפונקציה הזאת כפרוטוטייפ שלו. זה מסביר למה שיטות שמוגדרות על ה-prototype משותפות בין כל המופעים, בלי לתפוס זיכרון נפרד לכל אחד.
class - אותו דבר, תחביר נעים יותר¶
מ-ES6 ואילך, ג'אווהסקריפט מציעה תחביר class שנראה מוכר יותר למי שמגיע משפות אחרות. חשוב להבין - זו לא מערכת חדשה, זה בדיוק אותו מנגנון פרוטוטייפ, רק עטוף בתחביר נוח יותר לקריאה:
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} משמיע קול`);
}
}
const cat = new Animal("חתול");
cat.makeSound(); // "חתול משמיע קול"
אם תבדקו, makeSound עדיין יושבת על Animal.prototype, בדיוק כמו בגרסה עם פונקציית הבנאי. class פשוט קורא את הכוונה שלכם בצורה ברורה יותר.
ירושה עם extends ו-super¶
ירושה בין "מחלקות" עובדת עם extends, ומאפשרת לקומפוננטה חדשה להרחיב התנהגות קיימת:
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
makeSound() {
console.log(`${this.name} נובח`);
}
}
const rex = new Dog("רקס", "לברדור");
rex.makeSound(); // "רקס נובח"
super(name) קורא לבנאי של המחלקה שממנה יורשים, Animal, כדי שהוא יגדיר את this.name. makeSound ב-Dog "מחליף" (overrides) את הגרסה של Animal, כי ג'אווהסקריפט מוצאת קודם את הגרסה הקרובה יותר בשרשרת הפרוטוטייפים.
אז מתי כדאי class ומתי אובייקטים פשוטים¶
בקוד JavaScript מודרני, ובוודאי בריאקט, כמעט אף פעם לא כותבים class בעצמכם. קומפוננטות פונקציונליות והוקים החליפו לגמרי את קומפוננטות המחלקה הישנות. class עדיין שימושי בהקשרים ספציפיים - למידול נתונים מורכב, ספריות, או קוד שמתעד יחסי ירושה ברורים. אבל אם אתם רק צריכים לארגן נתונים ופונקציות קשורות, אובייקט פשוט או פונקציה עם סגירה (closure) לעיתים קרובות מספיקים בלי המורכבות הנוספת.
למה בכל זאת חשוב להבין את זה¶
גם אם לא כותבים class בעצמכם, שרשרת הפרוטוטייפים היא הבסיס לכל התנהגות ב-JavaScript. מערכים "יורשים" את השיטות שלהם (map, filter) מפרוטוטייפ מובנה, וכך גם מחרוזות ואובייקטים. להבין את המנגנון הזה עוזר להבין למה typeof [] הוא "object", ולמה שגיאות מסוימות של "התכונה לא קיימת" קורות דווקא איפה שהן קורות.
בקורס צד לקוח יש הסבר מלא על פרוטוטייפים לפני שמגיעים ל-class, בדיוק כדי שהבסיס יהיה מוצק ולא רק "תחביר ששיננתי".
לסיכום¶
פרוטוטייפים הם המנגנון האמיתי מאחורי ירושה בג'אווהסקריפט - כל אובייקט "מסתכל" על פרוטוטייפ אחר כשהוא לא מוצא תכונה בעצמו. class הוא רק תחביר נוח מעל אותו מנגנון בדיוק, לא מערכת נפרדת. הבנה של זה הופכת המון התנהגויות "מוזרות" ב-JS למובנות לגמרי.
מתלבטים בין class לאובייקטים פשוטים בפרויקט שלכם? תשאלו בדיסקורד.