4.3 פרוטוטייפים ומחלקות הרצאה
פרוטוטייפים ומחלקות¶
ב-JS, כל אובייקט מחובר לאובייקט אחר שנקרא ה-prototype שלו. כשמנסים לגשת למאפיין שלא קיים על אובייקט, JS מחפש אותו ב-prototype, וב-prototype של ה-prototype, וכך הלאה - שרשרת הפרוטוטייפ.
היום, בפרקטיקה, משתמשים ב-classes ולא מתעסקים ישירות עם prototypes. אבל חשוב להבין את המנגנון בקצרה כי classes ב-JS הם בסך הכל "סוכר תחבירי" (syntactic sugar) מעל prototypes.
פרוטוטייפים בקצרה¶
const animal = {
makeSound() {
console.log("Some sound");
}
};
const dog = Object.create(animal); // dog's prototype is animal
dog.name = "Rex";
dog.makeSound(); // "Some sound" - found on the prototype
console.log(dog.name); // "Rex" - found on dog itself
כש-JS מחפש את makeSound על dog:
1. בודק אם dog עצמו מכיל את makeSound - לא
2. בודק את ה-prototype של dog (שהוא animal) - כן, נמצא
console.log(dog.__proto__ === animal); // true
console.log(Object.getPrototypeOf(dog) === animal); // true (preferred way)
את __proto__ לא משתמשים בקוד פרודקשן - השתמשו ב-Object.getPrototypeOf().
שרשרת הפרוטוטייפ מסתיימת ב-null:
מחלקות - classes¶
מ-ES6, JS תומך בתחביר של classes. מאחורי הקלעים זה עדיין prototypes, אבל התחביר הרבה יותר נקי ומוכר:
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
makeSound() {
console.log(`${this.name} says ${this.sound}`);
}
}
const dog = new Animal("Rex", "Woof");
dog.makeSound(); // "Rex says Woof"
בפייתון זה כמעט זהה:
# class Animal:
# def __init__(self, name, sound):
# self.name = name
# self.sound = sound
#
# def make_sound(self):
# print(f"{self.name} says {self.sound}")
בנאי - constructor¶
ה-constructor הוא מתודה מיוחדת שרצה כשיוצרים מופע חדש עם new:
class User {
constructor(name, email) {
this.name = name;
this.email = email;
this.createdAt = new Date();
}
}
const user = new User("Alice", "alice@example.com");
console.log(user.name); // "Alice"
console.log(user.createdAt); // current date
- יכול להיות רק constructor אחד בכל class
- אם לא כותבים constructor, JS יוצר אחד ריק אוטומטית
thisבתוך constructor מצביע על המופע החדש שנוצר
מתודות - methods¶
מתודות מוגדרות ישירות בגוף ה-class:
class Calculator {
constructor(initialValue = 0) {
this.value = initialValue;
}
add(n) {
this.value += n;
return this; // enables chaining
}
subtract(n) {
this.value -= n;
return this;
}
multiply(n) {
this.value *= n;
return this;
}
getResult() {
return this.value;
}
}
const result = new Calculator(10)
.add(5)
.multiply(2)
.subtract(3)
.getResult();
console.log(result); // 27
מתודות סטטיות - static¶
מתודות static שייכות ל-class עצמו, לא למופעים. קוראים להן על ה-class ישירות:
class MathUtils {
static add(a, b) {
return a + b;
}
static random(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
static clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
}
console.log(MathUtils.add(2, 3)); // 5
console.log(MathUtils.random(1, 10)); // random number 1-10
console.log(MathUtils.clamp(15, 0, 10)); // 10
// can't call on instances:
// const m = new MathUtils();
// m.add(2, 3); // TypeError
שימוש נפוץ - factory methods:
class User {
constructor(name, email, role) {
this.name = name;
this.email = email;
this.role = role;
}
static createAdmin(name, email) {
return new User(name, email, "admin");
}
static createGuest() {
return new User("Guest", "guest@example.com", "guest");
}
}
const admin = User.createAdmin("Alice", "alice@example.com");
const guest = User.createGuest();
בפייתון, factory methods נעשים עם @classmethod או @staticmethod.
שדות פרטיים - private fields¶
שדות שמתחילים ב-# הם פרטיים - נגישים רק מתוך ה-class:
class BankAccount {
#balance;
#pin;
constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
}
#validatePin(pin) {
return pin === this.#pin;
}
deposit(amount) {
if (amount <= 0) throw new Error("Amount must be positive");
this.#balance += amount;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) throw new Error("Invalid PIN");
if (amount > this.#balance) throw new Error("Insufficient funds");
this.#balance -= amount;
}
getBalance(pin) {
if (!this.#validatePin(pin)) throw new Error("Invalid PIN");
return this.#balance;
}
}
const account = new BankAccount(1000, "1234");
account.deposit(500);
console.log(account.getBalance("1234")); // 1500
// private fields are truly private:
// console.log(account.#balance); // SyntaxError!
// account.#validatePin("1234"); // SyntaxError!
בפייתון, שדות פרטיים הם פשוט מוסכמה (קו תחתון בהתחלה _balance) - אפשר לגשת אליהם מבחוץ. ב-JS עם # זה פרטי באמת, אי אפשר לגשת מבחוץ.
גטרים וסטרים - getters and setters¶
מאפשרים להגדיר מאפיינים שמתנהגים כמו שדות אבל מריצים קוד:
class Circle {
constructor(radius) {
this.radius = radius;
}
get area() {
return Math.PI * this.radius ** 2;
}
get circumference() {
return 2 * Math.PI * this.radius;
}
get diameter() {
return this.radius * 2;
}
set diameter(d) {
this.radius = d / 2;
}
}
const circle = new Circle(5);
console.log(circle.area); // 78.539... (no parentheses!)
console.log(circle.circumference); // 31.415...
console.log(circle.diameter); // 10
circle.diameter = 20; // triggers the setter
console.log(circle.radius); // 10
שימו לב: קוראים ל-getter/setter בלי סוגריים, כאילו זה שדה רגיל. בפייתון, @property עושה את אותו דבר.
ירושה - extends¶
מחלקה יכולה לרשת ממחלקה אחרת עם extends:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
toString() {
return `Animal: ${this.name}`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // MUST call super() first!
this.breed = breed;
}
speak() {
console.log(`${this.name} barks`);
}
fetch(item) {
console.log(`${this.name} fetches the ${item}`);
}
}
const dog = new Dog("Rex", "Labrador");
dog.speak(); // "Rex barks" (overridden)
dog.fetch("ball"); // "Rex fetches the ball" (new method)
console.log(dog.toString()); // "Animal: Rex" (inherited)
super¶
super משמש לשני דברים:
1. ב-constructor - חייבים לקרוא ל-super() לפני שמשתמשים ב-this
2. במתודות - super.methodName() קורא למתודה של ההורה
class Animal {
constructor(name) {
this.name = name;
}
describe() {
return `${this.name} is an animal`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // call Animal's constructor
this.breed = breed;
}
describe() {
// call parent's describe and add to it
return `${super.describe()} (${this.breed})`;
}
}
const dog = new Dog("Rex", "Labrador");
console.log(dog.describe()); // "Rex is an animal (Labrador)"
בפייתון:
# class Dog(Animal):
# def __init__(self, name, breed):
# super().__init__(name)
# self.breed = breed
instanceof¶
בודק אם אובייקט הוא מופע של class מסוים (או של class שירש ממנו):
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
const dog = new Dog();
const cat = new Cat();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true (Dog extends Animal)
console.log(dog instanceof Cat); // false
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true
בפייתון, isinstance() עושה את אותו דבר:
דוגמה מסכמת¶
class Shape {
#color;
constructor(color = "black") {
this.#color = color;
}
get color() {
return this.#color;
}
set color(value) {
const validColors = ["red", "green", "blue", "black", "white"];
if (!validColors.includes(value)) {
throw new Error(`Invalid color: ${value}`);
}
this.#color = value;
}
area() {
throw new Error("area() must be implemented by subclass");
}
toString() {
return `${this.constructor.name} (${this.#color}), area: ${this.area().toFixed(2)}`;
}
static compare(shape1, shape2) {
return shape1.area() - shape2.area();
}
}
class Rectangle extends Shape {
constructor(width, height, color) {
super(color);
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
get perimeter() {
return 2 * (this.width + this.height);
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color);
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
get perimeter() {
return 2 * Math.PI * this.radius;
}
}
// usage
const rect = new Rectangle(10, 5, "red");
const circle = new Circle(7, "blue");
console.log(rect.toString()); // "Rectangle (red), area: 50.00"
console.log(circle.toString()); // "Circle (blue), area: 153.94"
console.log(Shape.compare(rect, circle)); // negative (rect is smaller)
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.color = "green"; // works
// rect.color = "purple"; // Error: Invalid color
השוואה לפייתון¶
| נושא | פייתון | ג׳אווהסקריפט |
|---|---|---|
| הגדרת class | class Dog: |
class Dog { } |
| בנאי | __init__(self) |
constructor() |
| הפניה עצמית | self (מפורש) |
this (מובנה) |
| ירושה | class Dog(Animal): |
class Dog extends Animal |
| קריאה להורה | super().__init__() |
super() ב-constructor |
| שדות פרטיים | מוסכמה _name |
#name (פרטי באמת) |
| מתודות סטטיות | @staticmethod |
static method() |
| getter/setter | @property |
get/set |
| בדיקת סוג | isinstance(obj, Class) |
obj instanceof Class |
| ירושה מרובה | נתמך | לא נתמך (אפשר mixins) |
ההבדל המרכזי: ב-JS, classes הם syntactic sugar מעל prototypes. בפייתון, classes הם מנגנון מובנה בשפה. בפרקטיקה ההבדל כמעט לא מורגש - התחביר דומה מאוד.