לדלג לתוכן

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:

// dog -> animal -> Object.prototype -> 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() עושה את אותו דבר:

# isinstance(dog, Dog)    # True
# isinstance(dog, Animal) # True

דוגמה מסכמת

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 הם מנגנון מובנה בשפה. בפרקטיקה ההבדל כמעט לא מורגש - התחביר דומה מאוד.