המאמר הזה מדבר על JavaScript ועל העצמים שלה. מי שמחפש מאמר למתחילים על תכנות מונחה עצמים בג'אווהסקריפט שייכנס למאמר שלי למתחילים שכתבתי לפני כמה שנים. המאמר הזה מיועד למתכנתים שמכירים תכנות מונחה עצמים ורוצים לדעת יותר על prototype chain וירושה בג'אווהסקריפט.
למרות שב-ES6 יש לנו קלאסים, מדובר ב'סוכר סינטקטי'. בניגוד לשפות אחרות, ג'אווהסקריפט היא לא מבוססת מחלקות אלא על פרוטוטייפ (בלעז prototype). ההבדל הוא דק אך משמעותי ויכול, ככל שאנחנו מתבססים יותר ויותר על JavaScript להיות משמעותי עבור הקוד שאתם כותבים. במאמר הזה אני אדבר על שרשרת פרוטוטייפ ב-JavaScript. אתם יותר ממוזמנים להעתיק את הקוד לקונסולה שלכם ולבדוק בעצמכם.
אז בואו ונתחיל עם אובייקט פשוט:
var o = {a: 1, b: 1}
אם נכניס את זה לקונסולה, אנו נראה את האובייקט, אם נחפור קצת יותר פנימה נראה משהו כזה:
מה זה __proto__? מדובר ב'אב הטיפוס' של מה שיצרנו. לכל דבר בג'אווהסקריפט יש אב טיפוס משלו. למשל, אם ניצור פונקציה נראה שה-__proto__ שלה הוא פונקציה, אבל ה-__proto__ של הפונקציה הוא object (בג'אווהסקריפט כמעט הכל מגיע ל-object). מה ה-__proto__ של object? פה מדובר ב-null. ל-nul אין פרוטוטייפ והוא הסוף של שרשרת הפרוטוטייפ.
במערכות ג'אווהסקריפט מורכבות אנחנו הרבה פעמים יוצרים אובייקטים. הרבה פעמים אנחנו קוראים להם 'מחלקות', אבל בג'אווהסקריפט אין מחלקות קלאסיות כמו בשפות אחרות אלא הכל בעצם הוא אובייקט (גם פונקציה זה אובייקט). אנחנו יכולים לרשת מאובייקט מסוים את התכונות שלו ולהוסיף תכונות משלנו.
בואו ונראה איך אנחנו יורשים מאובייקט מסוים. החל מ-ES5, מקובל לרשת אובייקטים באמצעות Object.create. בואו נראה דוגמה:
var o = {
a: 2,
m: function(b){
return this.a + 1;
}
};
var p = Object.create(o);
אובייקט p הוא אובייקט שיורש מ-o. אני יכול להסתכל על הירושה באופן הבא עם הקונסולה:
מה יקרה אם אני אעשה משהו כזה?
p.m(); //returns 3
אני אקבל 3, כיוון שזה מה בדיוק מה שיש באובייקט a, אובייקט p הוא העתק מושלם של אובייקט a. מה לפי דעתכם יקרה אם אני אעשה משהו כזה?
p.m(); //3
o.a = 10;
p.m(); //Now it is 11!
למה זה קורה? כי Object.create יוצר העתק מושלם, והתכונה a קיימת רק באובייקט המקורי. שינוי שלו זה שינוי בכל הפרוטוטייפ ואפשר לראות את זה אם מסתכלים בקונסולה:
למשתנה p יש רק __proto__, הוא אובייקט ריק שיש לו פרוטוטייפ, אבל אין לו כלום משל עצמו! לפיכך כל שינוי באב הטיפוס יגרור שינוי בכל אלו שיורשים ממנו. כאמור, ג'אווהסקיפט היא לא מבוססת מחלקות והדבר הזה מאוד מבלבל מי שרגיל לעבוד עם שפות כאלו.
אם אני אדרוס כמובן את תכונת הפרוטוטייפ, שינויים באובייקט המקורי כבר לא יחלחלו.
אחרי שעשינו override ל-a, הוא מתנתק מהפרוטוטייפ ויש לו זכות קיום משלו. זה הדבר הכי חשוב בכל המאמר הזה – שברגע שאני עושה override, הפרוטוטייפ כבר לא חשוב ולא מעניין. לפחות בנוגע לאובייקט שבו אני עובד כרגע.
שימו לב שזה עובד בנוגע לכל דבר בג'אווהסקריפט, מערך, פונקציה או אובייקט. הנה דוגמה ליצירת מערך ראשוני וירושה ממנו:
יש לזה כמובן כוח אדיר אבל גם סכנות מאוד גדולות. אם למשל יש לי אובייקט שיורש מאובייקט שיורש מאובייקט וכו' וכו' ואף אחד לא דרס תכונה או מתודה, שינויים שלהם יחלחלו לכל המערכת. אם אני מחפש תכונה מסוימת ולא מוצא אותה באובייקט, אני ישר הולך ל-__proto__ שלו, ואם אני לא מוצא אותו שם אני הולך ל-__proto__ של האבא וכך הלאה עד שאני מוצא או שאני מחזיר undefined.
על מנת לוודא שלאובייקט יש את התכונה שאני מחפש והערך לא מגיע מה-__proto__ אני יכול להשתמש ב hasOwnProperty
p.hasOwnProperty('a') //true
p.hasOwnProperty('m') //false, it is only in the __proto__
לצורך העניין פונקציה גם היא property.
נשאלת השאלה מה קורה אם אתה רוצה לרשת מאובייקט אבל ללא שרשרת פרוטוטייפ בכלל. כלומר רק לרשת את התכונות הקיימות? בדיוק בשביל זה יש ספריות עזר כמו lodash שיעזרו לביצוע cloning. ניתן גם להשתמש ב-constructor, שעובד עם new.
עבודה עם new היא סוג של מעקף או 'סוכר סינטקטי', כי בעצם אנחנו מנסים להמיר את ה-Prototypical Inheritance ב-פסאודו קלאס. כלומר משהו שנראה כמו קלאס, מתנהג כמו קלאס אבל מאחורי הקלעים הוא מתנהג בדיוק כמו אובייקט. איך עושים את זה? פשוט למדי:
function o() {
this.a = 1;
this.b = 2
}
o.prototype.myMethod = function() {
return this.a + this.b;
}
myObj = new o();
זה כבר משהו יותר מוכר. כשאנחנו משתמשים ב-new או ב-constructor, אנחנו מפעילים את כל קטע הקוד בתוך הפונקציה o. אם נסתכל על ה-__proto__ של myMethod, אנחנו נראה שהוא של o ומכיל את myMetho אבל לא את a או את b כלומר כל שינוי שנעשה בהם ב-o לא יחלחל ל-myObj אחרי שעשינו new. אבל אם נעשה שינוי ב-myMethod הוא יחלחל כמובן לכולם. אלא אם כן אני אדרוס אותו.
אפשר לסכם את זה שכשאנחנו עושים
myObj = new o();
אנחנו עושים משהו כזה:
var myObj = new Object();
myObj.__proto__ = o.prototype;
o.call(myObj);
ה-call באובייקט הוא call של ג'אווהסקריפט שעליו דיברתי במאמרים קודמים. מדובר בהפעלה של o עם קביעה של ה-this שיהיה בדיוק כמו זה של myObj.
אני יודע שהמאמר הזה נראה די תיאורטי, אבל ככל שאנחנו משתמשים יותר בפריימוורקים מורכבים, לתיאוריה הזו יש חשיבות. כשאנחנו משתמשים בירושות של scope באנגולר או במקומות אחרים מאוד קל לשבור את הראש על בעיות של חלחול מה-__proto__ בלי להבין מאיפה הדברים מגיעים. הבנה תיאורטית של דרך ההתנהגות של ג'אווהסקריפט ועל ה-prototype chain היא קריטית לכל מפתח.
תגובה אחת
חסר בעיני הסבר על ההבדל בין:
function o() {this.a = 1;this.b = 2;}
o.prototype.myMethod = function() {return this.a + this.b;}
ל:
function o() {
this.a = 1;this.b = 2;
this.myMethod = function() {return this.a + this.b;}
}