לכל אובייקט יש prototype
JavaScript היא שפה מבוססת prototypes. זה נשמע אקזוטי, אבל הרעיון פשוט: לכל אובייקט יש קישור סודי לאובייקט אחר, ה-prototype שלו, וכשמבקשים מאפיין שאין לאובייקט, JavaScript הולכת בעקבות הקישור ושואלת שם.
ל-rabbit אין מאפיין eats משלו. JavaScript בודקת את rabbit, לא מוצאת אותו, הולכת בעקבות קישור ה-prototype אל animal, מוצאת eats: true ומחזירה אותו. עבור flies, היא עוברת על כל השרשרת, לא מוצאת כלום ומחזירה undefined.
ההתנהגות הזו של חיפוש והליכה לאורך השרשרת היא כל המנגנון. ירושה, מתודות, class: הכול בנוי על זה.
שרשרת ה-prototypes
השרשרת לא נעצרת אחרי צעד אחד. ל-prototype יכול להיות prototype משלו, וכן הלאה, עד שמגיעים ל-null:
הריצו את הקוד ותראו את rabbit, אחריו Object.prototype, ואז null. זו הסיבה ש-rabbit.toString() עובד למרות שמעולם לא הגדרתם toString: הוא נמצא על Object.prototype, בראש כמעט כל שרשרת.
חיפוש מאפיין עולה בשרשרת הזו מלמטה למעלה. השמה, לעומת זאת, תמיד כותבת לאובייקט עצמו ואף פעם לא עולה למעלה. חוסר הסימטריה הזה חשוב, והוא מפתיע אנשים שוב ושוב.
פונקציות בנאי ו-.prototype
לפני ש-class הייתה קיימת, הדרך המקובלת ליצור הרבה אובייקטים דומים הייתה פונקציית בנאי שנקראת עם new:
שני דברים קורים כשקוראים ל-new User("Ada"):
- נוצר אובייקט חדש, וה-prototype שלו מוגדר ל-
User.prototype. Userרצה כש-thisקשור לאובייקט החדש הזה.
greet לא מועתקת לכל מופע. היא קיימת פעם אחת על User.prototype, וגם ada וגם boris מוצאים אותה כשהם עולים בשרשרת שלהם. לכן השורה האחרונה מדפיסה true: זו ממש אותה פונקציה.
prototype מול __proto__
שני השמות האלה מבלבלים את כולם. הם קשורים, אבל הם לא אותו דבר.
User.prototypeהוא מאפיין של פונקציית הבנאי. זה האובייקט שהופך ל-prototype של מופעים שנוצרים עםnew User(...).ada.__proto__(אוObject.getPrototypeOf(ada)) הוא הקישור על המופע שמצביע למעלה אל ה-prototype שלו.
בקוד חדש, העדיפו Object.getPrototypeOf(obj) על פני obj.__proto__. __proto__ הוא accessor ישן שנשמר לצורך תאימות; הפונקציה היא ה-API הרשמי.
מחלקות הן סוכר מעל זה
JavaScript מודרנית מאפשרת לכתוב class, אבל מתחת עדיין מדובר ב-prototypes. השוו את שתי הגרסאות זו לצד זו:
greet נחתה על User.prototype, בדיוק כאילו כתבתם אותה ביד. מילת המפתח class נותנת בעיקר תחביר מסודר יותר, כללים מחמירים יותר (חייבים להשתמש ב-new) ודרך נקייה יותר לעשות extends, אבל מודל זמן הריצה זהה.
חשוב לדעת את זה כשקוראים הודעות שגיאה או מדבגים את this. שגיאה על "User.prototype.greet" היא לא שם פנימי מוזר: זה בדיוק המקום שבו המתודה נמצאת.
ירושה היא פשוט שרשרת ארוכה יותר
extends מחבר prototype אחד לאחר. ה-prototype של מחלקת האב הופך ל-prototype של ה-prototype של מחלקת הבת:
החיפוש של rex.eat עובר דרך rex → Dog.prototype → Animal.prototype, מוצא שם את eat וקורא לה כש-this עדיין קשור ל-rex. זה כל מה ש-extends עושה: הוא בונה בשבילכם את השרשרת.
יצירת אובייקטים ישירות עם prototype
לא צריך בנאי בכלל. Object.create(proto) יוצר אובייקט חדש עם ה-prototype שתבחרו:
בלי class, בלי new, בלי פונקציית בנאי. שני אובייקטים שחולקים מתודה אחת דרך prototype משותף. זו הצורה הבסיסית ביותר של ירושה מבוססת prototypes, וכל השאר בנוי מעליה.
hasOwnProperty: משלו מול בירושה
מכיוון שחיפושים עולים בשרשרת, "foo" in obj מחזיר true גם עבור מאפיינים שהתקבלו בירושה. כשצריך להבחין במאפיין שבאמת שייך לאובייקט, השתמשו ב-Object.hasOwn (או ב-hasOwnProperty הישן יותר):
name נמצא על המופע. greet נמצאת על ה-prototype. in מוצא את שניהם; Object.hasOwn מוצא רק את הראשון. זה חשוב בכל פעם שעוברים על אובייקט עם for...in או ממירים אותו לטקסט (serialize): בדרך כלל רוצים רק את המאפיינים של האובייקט עצמו.
אל תשנו prototypes מובנים
מכיוון ש-Array.prototype משותף לכל מערך בתוכנית, אתם יכולים להוסיף לו מתודות:
// Please don't.
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3
הבעיה היא לא שזה לא עובד. זה עובד. הבעיה היא שכל ספרייה, כל תלות וכל גרסה עתידית של JavaScript חולקות עכשיו איתכם את מרחב השמות הזה. כש-Array.prototype.last תגיע בסוף כמתודה אמיתית עם סמנטיקה מעט שונה, הקוד שלכם (או של מישהו אחר) יישבר בדרכים עדינות. הסאגה של Array.prototype.flatten / Array.prototype.flat היא סיפור האזהרה הקלאסי.
שמרו פונקציות עזר כפונקציות עצמאיות:
משטח משותף אחד פחות שאפשר להתנגש בו.
המודל המחשבתי
אם מורידים את כל השאר, prototypes מסתכמים בשלושה כללים:
- לכל אובייקט יש קישור prototype (שיכול להיות
null). - קריאת מאפיין עולה בשרשרת; כתיבה לא.
class,newו-extendsהם דרכים לבנות את השרשראות האלה בלי לכתובObject.createבעצמכם.
זכרו את שלושת הכללים האלה, וההתנהגות של this, instanceof, מציאת מתודות וירושה תתבהר מעצמה.
הבא בתור: ה-Event Loop
prototypes סוגרים את נושא מודל האובייקטים. הפרק הבא עובר למשהו אחר לגמרי: איך JavaScript באמת מריצה את הקוד שלכם לאורך זמן. ה-event loop הוא מה שגורם לטיימרים, ל-Promises ול-async/await להתנהג כמו שהם מתנהגים, והוא הבסיס לכל מה שאסינכרוני.
שאלות נפוצות
מה זה prototype ב-JavaScript?
לכל אובייקט ב-JavaScript יש קישור פנימי לאובייקט אחר שנקרא ה-prototype שלו. כשניגשים למאפיין שלא נמצא על האובייקט עצמו, JavaScript עולה לאורך הקישור הזה, שרשרת ה-prototypes, ומחפשת אותו שם. השרשרת הזו היא הדרך שבה מתודות שהוגדרו פעם אחת משותפות בין מופעים רבים.
מה ההבדל בין __proto__ ל-prototype?
prototype הוא מאפיין של פונקציות בנאי (ושל מחלקות). זה האובייקט שיהפוך ל-prototype של מופעים שנוצרים עם new. __proto__ (או Object.getPrototypeOf(obj)) הוא הקישור עצמו על המופע, שמצביע על ה-prototype שלו. לכן instance.__proto__ === Constructor.prototype.
האם מחלקות ב-JavaScript הן רק סוכר תחבירי מעל prototypes?
ברובן, כן. class Foo { bar() {} } שם את bar על Foo.prototype, בדיוק כאילו כתבתם function Foo(){} ו-Foo.prototype.bar = function(){}. מחלקות מוסיפות שדות פרטיים, סמנטיקה מחמירה יותר ותחביר נוח יותר ל-extends ול-super, אבל המנגנון שמתחת הוא עדיין prototypes.
כדאי להוסיף מתודות ל-prototypes מובנים כמו Array.prototype?
כמעט אף פעם לא. שינוי של Array.prototype או של Object.prototype משפיע על כל מערך או אובייקט בתוכנית, כולל אלה שמגיעים מספריות. זה יכול להתנגש בתוספות עתידיות לשפה ולשבור לולאות for...in. שמרו פונקציות עזר בפונקציות או במודולים משלהן.