--- title: Inheritance in JavaScript slug: Learn/JavaScript/Objects/Inheritance translation_of: Learn/JavaScript/Objects/Inheritance ---
לאחר שסיימנו עם התיאוריה והפרטים של OOJS, המאמר הזה נועד להראות כיצד ליצור מחלקות אובייקטים ״ילדים״ (constructors) אשר יורשים מההורים שלהם. אנו גם נציג מעין עצות מתי נרצה להשתמש ב-OOJS ונסתכל כיצד ״מחלקות״ מתבצעות בסינטקס המודרני של ECMAScript.
ידע מוקדם: |
אוריינות מחשב בסיסית, הבנה בסיסית ב-HTML ו ב-CSS, היכרות עם יסודות ה-JavaScript (ראה First steps and Building blocks) and OOJS basics (see Introduction to objects). |
---|---|
מטרה: | להבין כיצד זה אפשרי להחיל הורשה ב- JavaScript |
עד עכשיו ראינו קצת הורשה בפעולה - ראינו כיצד prototype chains עובדות וכיצד מתודות ופרופ׳ מורשים בהמשך השרשרת. אבל זה היה בעיקר עם פונקציות מובנות של הדפדפן. כיצד אנחנו יכולים ליצור אובייקט ב-JavaScript אשר יורש מאובייקט אחר?
ראשית, עשו עותק מקומי של הקובץ או ראו אותו כדף אינטרנט. בקוד זה אתם תראו את ה-Person()
constructor שהשתמשנו בו לאורך המודול, עם שינוי קל - הגדרנו רק את ה-properties בתוך ה-constructor.
function Person(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; };
כל המתודות כולן מוגדרות בתוך ה-constructor's prototype. לדוגמא:
Person.prototype.greeting = function() { alert('Hi! I\'m ' + this.name.first + '.'); };
לתשומת לבך: בקוד המקור אתם גם תראו מתודות נוספות שמוגדרות, ()bio
ו-()farewell
. אנחנו נראה בהמשך כיצד מתודות אלו יכולות להיות מורשות ל-constructors אחרים.
נניח ואנחנו רוצים ליצור מחלקה של Teacher
, כמו זו שהסברנו בתחילת המודול לגבי תכנות מונחה עצמים, ומחלקה זו יורשת את כל הפרופ׳ והמתודות של Person
, אבל גם כוללת:
subject
— אשר יכיל את נושאי הלימוד שהמורה מלמד.()greeting
מעודכנת, אשר תהיה יותר רשמית מהמתודה ()greeting
הרגילה.הדבר הראשון שאנחנו צריכים לעשות הוא להגדיר את ה-constructor בשם ()Teacher
- הוסיפו את הקוד הבא מתחת לקוד הנוכחי:
function Teacher(first, last, age, gender, interests, subject) { Person.call(this, first, last, age, gender, interests); this.subject = subject; }
זה נראה מאוד דומה ל-constructor בשם Person, אבל משהו פה שונה, משהו שלא ראינו עד כה - פונקציה בשם ()call
.
פונקציה זו היא בעיקרון מאפשרת לנו לקרוא לפונקציה שהוגדרה במקום אחר, אבל לקרוא לה בהקשר הנוכחי.
הפרמטר הראשון שהפונקציה הזו מקבלת מכיל את הערך של this
שאנחנו נרצה להשתמש בו כאשר אנחנו מריצים את הפונקציה, והפרמטרים האחרים אלו פרמטרים שאמורים להיות מועברים לפונקציה עצמה כאשר היא מופעלת.
אנחנו רוצים שה-Teacher()
constructor יקבל את אותם פרמטרים כמו שה-Person()
constructor שהוא יורש ממנו מקבל, ולכן אנחנו מציינים אותם כפרמטרים בתוך ההפעלה של ה-()call
.
השורה האחרונה בתוך ה-constructor פשוט מגדירה property בשם subject
אשר ייחודי למורים, שאנשים רגילים מ-()Person
לא מקבלים.
יכלנו גם לרשום זאת כך, ללא שימוש ב-()call
:
function Teacher(first, last, age, gender, interests, subject) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; this.subject = subject; }
אבל בקוד למעלה זו הגדרה מחדש של ה-properties כחדשים, לא כאלו שיורשים מ- ()Person
, אז זה סותר את מה שאנחנו מנסים לעשות - זה לא מוריש אלא זה יוצר חדשים. מה גם שזה לוקח יותר שורות קוד.
שימו לב שאם ה-constructor שאנחנו יורשים ממנו לא מקבל את הערכים של ה-property שלו מקבלים מפרמטרים, אז אנחנו לא צריכים לציין אותם כפרמטרים בתוך ה-()call
. לדומא, אם היה לנו משהו פשוט כמו זה: :
function Brick() { this.width = 10; this.height = 20; }
אנחנו יכולים לרשת את ה-properties של ה-width
ואת ה-height
, באמצעות שימוש בקוד הרשום מטה:
function BlueGlassBrick() { Brick.call(this); this.opacity = 0.5; this.color = 'blue'; }
שימו לב שאנחנו רק צריכים לציין את ה-this
בתוך ה-()call
, ללא פרמטרים נוספים, שכן אנחנו לא יכולים יורשים שום דבר מ-()Brick
שהוא קיבל דרך פרמטרים.
עד עכשיו הכל עובד תקין, אך יש לנו בעיה. הגדרנו אמנם constructor חדש, ויש לו את ה-property בשם prototype
, אשר כברירת מחדל מכיל רק הפנייה ל-constructor function עצמה.
הוא לא מכיל שום מתודות של ה-property בשם prototype
של ה-Person constructor. על מנת לראות זאת, הכניסו (Object.getOwnPropertyNames(Teacher.prototype
לתוך הקונסולה.
לאחר מכן הכניסו זאת שוב, והחליפו את המילה Teacher
במילה Person
. ה-constructor החדש לא יורש את אותן מתודות. על מנת לראות זאת, השוו את הפלט של Person.prototype.greeting
והפלט של Teacher.prototype.greeting
. אנחנו צריכים לגרום ל-()Teacher
לירוש מתודות שמוגדרות ב-prototype של ()Person
. איך עושים זאת?
Teacher.prototype = Object.create(Person.prototype);כאן
()create
מגיע שוב לעזרה. במקרה הזה, אנחנו משתמשים בו על מנת ליצור אובייקט חדש שיהיה הערך של Teacher.prototype
. האובייקט החדש הוא בעל Person.prototype
כאובייקט ה-prototype שלו, ולכן, הוא יירש ממנו אם וכאשר יצטרך, את כל המתודות שזמינות ב-Person.prototype
.constructor
שווה כעת ל- ()Person
, מכיוון שאנחנו הרגע הגדרנו את Teacher.prototype
אליו. נסו לשמור את הקוד ולהעלות את הדף בדפדפן וראו זאת על ידי הקלדת Teacher.prototype.constructor
בקונסולה.Object.defineProperty(Teacher.prototype, 'constructor', { value: Teacher, enumerable: false, // so that it does not appear in 'for in' loop writable: true });
Teacher.prototype.constructor
לקונסולה אמורה להחזיר לכם ()Teacher
, כפי שרצינו, ובנוסף אנחנו יורשים מ-()Person
.אנו רוצים להגדיר פונקציית ()greeting
חדשה בתוך ה-Teacher()
constructor שלנו.
הדרך הפשוטה ביותר לעשות זאת היא להגדיר זאת בתוך ה-prototype של ()Teacher
- הוסיפו את הקוד הבא מתחת לקוד הנוכחי:
Teacher.prototype.greeting = function() { var prefix; if (this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') { prefix = 'Mr.'; } else if (this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') { prefix = 'Mrs.'; } else { prefix = 'Mx.'; } alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.'); };
מתודה זו מקפיצה את הברכה של המורה, ומשתמשת במילים הנכונות בהתאם למין המורה באמצעות משפטי תנאי שהוגדרו.
כעת שהכנסו את כל הקוד, נסו ליצור אובייקטים חדשים מ-()Teacher
באמצעות הכנסת הקוד הבא מתחת לקוד הנוכחי:
var teacher1 = new Teacher('Dave', 'Griffiths', 31, 'male', ['football', 'cookery'], 'mathematics');
כעת, שמרו את הדף ורעננו ונסו לגשת לפרופ׳ והמתודות של האובייקט teacher1
החדש שלנו. לדוגמא:
teacher1.name.first; teacher1.interests[0]; teacher1.bio(); teacher1.subject; teacher1.greeting(); teacher1.farewell();
הכל אמור לעבוד כשורה. השורות 1,2,3 ו-6 משתמשות במתודות/פרופ׳ שנורשו מה-Person()
constructor שלנו. השורה 4 משתמשת בפרופ׳ שזמין רק באמצעות ה-Teacher()
constructor שלנו. שורה 5 הייתה יכולה להיות מורשת מ-()Person
, אבל מכיוון של-()Teacher
יש את אותה מתודה, אז היא ניגשת למתודה שנמצאת ב-()Teacher
.
לתשומת לב: אם נתקלתם בבעיות, נסו להשוות את הקוד שלכם ל קוד הסופי או ראו אותו כ דף אינטרנט.
הטכניקות שראינו עד כה, אינן היחידות ליצירת מחלקות ירושה ב-JavaScript, אבל הן עובדות בסדר, הן נותנות לכם הבנה כיצד להחיל ירושה ב-JavaScript.
אולי תהיו מעוניינים לבדוק חלק מהאפשרויות החדשות ש-{{glossary("ECMAScript")}} מאפשרת לנו, בצורה ״נקייה״ יותר, באמצעות Classes. אנו נגע בדרך זו בהמשך. שימו לב כי דרך חדשה זו לא תומכת בכל הדפדפנים. כל יתר הדרכים שראינו תומכות בדפדנים ישנים גם כמו IE9 ומוקדם יותר ויש דרכים גם לאפשר תמיכה לדפדפנים ישנים יותר.
דרך מקובלת היא להשתמש בספריית JavaScript - לפופולריות שביניהן יש סט של פונקציונליות שזמין עבור ביצוע הורשה בצורה פשוטה ומהירה. אחת מהן היא CoffeeScript אשר מאפשרת class
,extends
לדוגמא.
במאמר שלנו בנושא OOP theory section, כללנו גם מחלקת Student
באופן עקרוני, אשר יורשת תכונות מ-Person
וגם יש לה מתודת ()greeting
שונה מזו של Person
ומשל Teacher
. נסתכל כיצד ה-greeting של התלמידים אמורה להיות וננסה לבנות constructor בשם ()Student
משלנו, אשר יורש את כל התכונות מ-()Person
ומחיל מתודת ()greeting
שונה.
שימו לב: אם אתם נתקלים בבעיות, ראו את הגרסה הסופית או כדף אינטרנט .
על מנת לסכם, יש לנו בעיקרון שלוש סוגים של property/method לדאוג להם:
this
. אלו בעיקרון מאוד קלים לאיתור - בתוך הקוד שלנו, אלו התכונות שמוגדרות בתוך ה-constructor באמצעות this.x = x
, והם זמינים רק עבור האובייקטים שנוצרים (בדרך כלל נוצרים באמצעות ה-constructor ושימוש במילה השמורה new
, לדוגמא: ()var myInstance = new myConstructor
.()Object.keys
.()myConstructor.prototype.x
.אם אתם לא בטוחים מה זה מה, אל תדאגו, אתם תכירו אותם יותר לעומק במהלך הדרך והמשך הקריירה שלכם ככל שתתמודדו איתם.
ECMAScript 2015 הציגה מעין סינטקס חדש בשם class syntax ל- JavaScript כדרך חדשה לרשום מחלקות לשימוש חוזר, באמצעות סינטקס פשוט ונקי יותר, אשר דומה יותר ל-classes ב-C++ או ב-Java. בחלק הזה של המאמר אנחנו נמיר את הדוגמאות מלמעלה מ-prototypal inheritance ל-classes, על מנת להראות לכם איך לעשות שימוש-classes.
לתשומת לב: דרך חדשה זו של כתיבת classes נתמכת בכל הדפדפנים המודרניים, אבל זה עדיין חשוב להבין את ה-prototypal inheritance במקרה ותיתקלו בפרוייקט שדורש תמיכה בדפדפן שאינו תומך בסינטקס של classes - הידוע מבין כולם הוא Internet Explorer.
נסתכל על הדוגמא שלנו של Person כתובה בצורת classes:
class Person { constructor(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; } greeting() { console.log(`Hi! I'm ${this.name.first}`); }; farewell() { console.log(`${this.name.first} has left the building. Bye for now!`); }; }
ההצהרה באמצעות המילה השמורה class מצהירה בעצם בשאנחנו רוצים ליצור class חדשה. בתוך הבלוק הזה שבין {}
, אנחנו מגדירים את התכונות של אותה מחלקה:
()constructor
מגדירה את ה-constructor function שמייצגת את ה-class Person
שלנו.()greeting
ו- ()farewell
הם class methods. כל מתודה שאנחנו נרצה לייחס אותה למחלקה מוגדרת בתוך ה-class, לאחר ה-constructor. בדוגמא הזו, השתמשנו ב- template literals מאשר בשרשור מחרוזות על מנת שהקוד שלנו יהיה קריא יותר.כעת אנחנו יכולים ליצור מופעי אובייקט חדשים באמצעות שימוש באופרטור new
operator, באותה הדרך שעשינו בעבר:
let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']); han.greeting(); // Hi! I'm Han let leia = new Person('Leia', 'Organa', 19, 'female', ['Government']); leia.farewell(); // Leia has left the building. Bye for now
לתשומת לב: מאחורי הקלעים, ה-classes שלנו מומרים ל-prototypal Inheritance models - זהו רק syntactic sugar - שזה אומר דרך אחרת מבחינת סינטקס לעשות דבר זהה, אך לא משהו אחר. יחד עם זאת, אין ספק שזו דרך פשוטה יותר להעניק הורשה.
למעלה יצרנו class על מנת לתאר person. יש לנו סט של תכונות שזהות לכל האנשים. בחלק זה אנחנו ניצור את ה-class המיוחד של Teacher
, ונגרום לו לירוש מ-Person
באמצעות שימוש בסינטקס class החדש. זה נקרא יצירת subclass או ביצוע subclassing.
על מנת ליצור subclass אנחנו יכולים להשתמש במילה השמורה extends על מנת להגיד ל-JavaScript איזו class אנחנו מתבססים עליה ביצירת ה-class החדשה:
class Teacher extends Person { constructor(subject, grade) { this.subject = subject; this.grade = grade; } }
אך יש קאצ׳ קטן:
שלא כמו ה-Constructor function, שבהן האופרטור new
operator היה עושה את האתחול של המילה this
לאובייקט חדש, פה זה לא קורה בצורה אוטומטית בעבור המחלקה שמוגדרת באמצעות המילה extends, כלומר עבור ה-sub-classes.
ולכן הרצה שלהקוד למעלה יציג לנו שגיאה:
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
למחלקות משנה, sub-classes, ההגדרה של this
בעבור אובייקט חדש, תהיה תמיד תלויה ב-parent class constructor, כלומר ב-constructor function של ה-class שממנה אנחנו מתרחבים (extending).
כאן, אנחנו מרחיבים את המחלקה Person
- המחלקת משנה בשם -Teacher
היא בעצם extension - הרחבה - של המחלקה Person
. אז עבור Teacher
האתחול של this
מתבצע על ידי ה-constructor Person
.
על מנת לקרוא ל-constructor ה-parent, אנחנו צריכים להשתמשש באופרטור ()super
, כך:
class Teacher extends Person { constructor(subject, grade) { super(); // Now `this` is initialized by calling the parent constructor. this.subject = subject; this.grade = grade; } }
אין שום סיבה שתהיה לנו sub-class אם היא לא יורשת מאפיינים מה-parent class.
זה רעיון טוב אז שהאופרטור ()super
גם מקבל ארגומנטים בעבור ה- parent constructor.
כאשר אנחנו מסתכלים על ה- Person
constructor, אנחנו יכולים לראות שיש לו את הבלוק קוד הבא בתוך ה-constructor שלו:
constructor(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; }
מאחר והאופרטור ()super
הוא בעצם ה-parent class constructor, העברה של הארגומנטים הרלוונטיים של המחלקת אם, גם תאתחל את הפרופ׳ במחלקת משנה שלנו, ותירש אותם:
class Teacher extends Person { constructor(first, last, age, gender, interests, subject, grade) { super(first, last, age, gender, interests); // subject and grade are specific to Teacher this.subject = subject; this.grade = grade; } }
כעת, כשיוצרים מופעי אובייקט של Teacher
, אנחנו יכולים לקרוא למתודות ופרופ׳ שהוגדרו גם ב-Teacher
וגם ב-Person
:
let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5); snape.greeting(); // Hi! I'm Severus. snape.farewell(); // Severus has left the building. Bye for now. snape.age // 58 snape.subject; // Dark arts
לתשומת לב: אתם יכולים למצוא את הדוגמא ב-GitHub ב- es2015-class-inheritance.html או כ-דף אינטרנט.
יהיו פעמים שאנחנו נרצה לשנות את הערכים של מאפיין בתוך השאנחנו יוצרים או שאנחנו לא נדע מהו הערך הסופי שאותו מאפיין יקבל. אם נסתכל על הדוגמא שלנו Teacher
, יכול להיות מצב שאנחנו לא נדע מה הנושא שהמורה מלמד לפני שאנחנו יוצרים אותו או שהנושא יכול להשתנות במהלך התקופה.
במקרים כאלו אנחנו נוכל להשתמש ב-getters ו-setters.
נשפר את Teacher class עם getters ו-setters. המחלקה מתחילה בדיוק כמו שראינו אותה בדוגמא האחרונה.
getters ו-setters עובדים בזוגות. getter מחזיר את הערך הנוכחי של משתנה וה-setter הבן זוג שלו משנה את הערך של המשתנה למה שה-setter יגדיר.
המחלקה Teacher
החדשה תיראה כך:
class Teacher extends Person { constructor(first, last, age, gender, interests, subject, grade) { super(first, last, age, gender, interests); // subject and grade are specific to Teacher this._subject = subject; this.grade = grade; } get subject() { return this._subject; } set subject(newSubject) { this._subject = newSubject; } }
במחלקה למעלה יש לנו getter ו-setter בעבור הפרופ׳ subject
. אנחנו משתמשים בסימן _
על מנת ליצור ערך נפרד שבו נאחסכן את השם של הפרופ׳. אם לא נעשה זאת בצורה הזה, אנחנו נקבל שגיאות בכל פעם שנקרא ל-get או ל-set. בנקודה זו:
_subject
של האובייקט snape
, אנחנו יכולים להשתמש במתודת snape.subject
getter._subject
אנחנו יכולים להשתמש במתודת snape.subject="new value"
setter. הדוגמא למטה מראה את השימוש באפשרויות האלו:
// Check the default value console.log(snape.subject) // Returns "Dark arts" // Change the value snape.subject="Balloon animals" // Sets _subject to "Balloon animals" // Check it again and see if it matches the new value console.log(snape.subject) // Returns "Balloon animals"
לתשומת לב : תוכלו למצוא את es2015-getters-setters.html ב-GitHub, או כדף אינטרנט.
רוב הסיכויים שלאחר קריאת המאמר הזה, אתם בטח חושבים לעצמכם ״טוב, זה מסובך מאוד״. אתם צודקים. הורשה ואבי טיפוס הינם חלק מההיבטים המורכבים ביותר של JavaScript, אבל הרבה מעוצמתה של השפה והגמישות שלה מגיע מתוך המבנה של האובייקטים והירושה שלהם, וזה שווה להכיר ולהבין כיצד התהליכים הללו מתרחשים.
בדרך מסויימת, אנחנו משתמשים בהורשה כל הזמן. בכל פעם שאנחנו משתמשים במאפיינים שונים של Web API או בפרופ׳/מתודות שהוגדרו באובייקט מובנה של הדפדפן (built-in browser object) על מחרוזות, מערכים וכד׳ אנחנו באופן עקיף משתמשים בירושה.
במונחים של שימוש בירושה בקוד שלנו, אנחנו ככל הנראה לא נשתמש בזה באופן תדיר, במיוחד בהתחלה ובפרוייקטים קטנים. זה בזבוז זמן להשתמש באובייקטים וירושה רק לשם השימוש בהם אם אנחנו לא צריכים. אבל ככל שכמות הקוד גדלה, אנחנו ככל הנראה נזהה את הצורך להתחיל להשתמש בכך. םא אנחנו מוצאים את עצמנו מתחילים ליצור מספר אובייקטים שיש להם מאפיינים זהים, אז יצירת אובייקט גנרי אחד אשר יכיל את כל הפונקציונליות המשותפת של אותם אובייקטים ויוריש להם את אותה פונקציונליות תהיה דרך מאוד שימושית ונוחה.
לתשומת לב: לאור הדרך שבה JavaScript עובדת עם שרשרת אבי הטיפוס (prototype chain) וכד׳ - השיתוף של פונקציונליות בין אובייקטים נקרא לרוב delegation - ״האצלה״. אובייקטים מיוחדים ״מאצילים״ פונקציונליות לאובייקטים שנוצרים.
כאשר אנחנו משתמשים בהורשה, ההמלצה היא שלא יהיו יותר מדי רמות של הורשה, ושתמיד נעקוב איפה אנחנו מגדירים את המתודות והפרופ׳. זה אפשרי להתחיל לכתוב קוד שבאופן זמני משנה את ה-prototypes של האובייקטים המובנים של הדפדפן (built-in browser objects), אבל אין לעשות זאת אלא אם כן יש לנו סיבה מאוד טובה. יותר מדי הורשה יכולה להוביל לבלבול אינסופי ולשגיאות בקוד.
באופן אולטמטיבי, אובייקטים הם פשוט תבנית אחרת של שימוש חוזר בקוד, כמו פונקציות ולולאות, עם הכללים והיתרונות שלהם. אם אתם מוצאים את עצמכם יוצרים משתנים ופונקציות הקשורים אחד לשני ואתם רוצים לעקוב ולארוז אותם יחד בצורה מסודרת, אובייקט הוא רעיון טוב. אובייקטים גם שימושיים מאוד כשאנחנו רוצים להעביר ריכוז של מידע ממקום אחד למקום שני. את שני הדברים הללו ניתן להשיג ללא שימוש ב-constructors או ב-inheritance. אם אנחנו צריכים רק מופע אחד של אובייקט, כנראה יהיה עדיף פשוט להשתמש ב-inheritance ואין צורך בירושה.
ב-JavaScript, יש מספר דרכים שונות להרחבה של ה-prototype של אובייקט חוץ מאלו שראינו במאמר זה. להרחבה בנושא, ראו את הדף שלנו בנושא Inheritance and the prototype chain.
מאמר זה נועד לסקור את יתרת הנושא של OOJS וסינטקס נוסף שאנחנו חושבים שאתם צריכים לדעת. בנקודה זו אתם אמורים להבין את ההיבטים של אובייקטים ב-JavaScript ואת הבסיס של תכנות מונחה עצמים (OOP), אביט טיפוס, שרשרת אבי-טיפוס, הורשה באמצעות אבי-טיפוס, כיצד ליצור מחלקות (classes), מופעי אובייקטים, הוספת מאפיינים למחלקות, יצירת מחלקות משנה שיורשות ממחלקות אחרות ועוד.
במאמר הבא אנחנו נגע כיצד לעבוד עם (JavaScript Object Notation (JSON, פורמט מקובל להעברת מידע.
{{PreviousMenuNext("Learn/JavaScript/Objects/Object_prototypes", "Learn/JavaScript/Objects/JSON", "Learn/JavaScript/Objects")}}