לדלג לתוכן

4.3 פרוטוטייפים ומחלקות פתרון

פתרון - פרוטוטייפים ומחלקות


תרגיל 1 - מחלקת Stack

class Stack {
    #items = [];

    push(item) {
        this.#items.push(item);
    }

    pop() {
        if (this.isEmpty()) {
            throw new Error("Stack is empty");
        }
        return this.#items.pop();
    }

    peek() {
        if (this.isEmpty()) {
            throw new Error("Stack is empty");
        }
        return this.#items[this.#items.length - 1];
    }

    isEmpty() {
        return this.#items.length === 0;
    }

    get size() {
        return this.#items.length;
    }

    clear() {
        this.#items = [];
    }

    toArray() {
        return [...this.#items];
    }
}

// test
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.peek());    // 3
console.log(stack.pop());     // 3
console.log(stack.size);      // 2
console.log(stack.toArray()); // [1, 2]
stack.clear();
console.log(stack.isEmpty()); // true

תרגיל 2 - ירושה - צורות גיאומטריות

class Shape {
    constructor(color) {
        this.color = color;
    }

    area() {
        throw new Error("area() must be implemented by subclass");
    }

    describe() {
        return `${this.constructor.name} with area ${this.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;
    }
}

class Square extends Rectangle {
    constructor(side, color) {
        super(side, side, color);
    }
}

// test
const rect = new Rectangle(10, 5, "red");
console.log(rect.area());     // 50
console.log(rect.perimeter);  // 30
console.log(rect.describe()); // "Rectangle with area 50"

const circle = new Circle(7, "green");
console.log(circle.area().toFixed(2));    // "153.94"
console.log(circle.perimeter.toFixed(2)); // "43.98"

const square = new Square(4, "blue");
console.log(square.area());              // 16
console.log(square.perimeter);           // 16
console.log(square instanceof Rectangle); // true
console.log(square instanceof Shape);     // true

תרגיל 3 - מחלקה עם שדות פרטיים ו-validation

class Temperature {
    #celsius;

    constructor(celsius) {
        this.celsius = celsius; // use the setter for validation
    }

    get celsius() {
        return this.#celsius;
    }

    set celsius(value) {
        if (value < -273.15) {
            throw new Error("Temperature cannot be below absolute zero (-273.15C)");
        }
        this.#celsius = value;
    }

    get fahrenheit() {
        return this.#celsius * 9 / 5 + 32;
    }

    set fahrenheit(f) {
        this.celsius = (f - 32) * 5 / 9;
    }

    get kelvin() {
        return this.#celsius + 273.15;
    }

    set kelvin(k) {
        this.celsius = k - 273.15;
    }

    static fromFahrenheit(f) {
        return new Temperature((f - 32) * 5 / 9);
    }

    static fromKelvin(k) {
        return new Temperature(k - 273.15);
    }

    toString() {
        return `${this.#celsius}C / ${this.fahrenheit}F / ${this.kelvin}K`;
    }
}

// test
const t = new Temperature(100);
console.log(t.fahrenheit); // 212
console.log(t.kelvin);     // 373.15
console.log(t.toString()); // "100C / 212F / 373.15K"

t.fahrenheit = 32;
console.log(t.celsius); // 0

const t2 = Temperature.fromKelvin(0);
console.log(t2.celsius); // -273.15

// validation
// new Temperature(-300); // Error: Temperature cannot be below absolute zero

תרגיל 4 - מחלקה LinkedList

class Node {
    constructor(value) {
        this.value = value;
        this.next = null;
    }
}

class LinkedList {
    #head = null;
    #size = 0;

    append(value) {
        const newNode = new Node(value);

        if (!this.#head) {
            this.#head = newNode;
        } else {
            let current = this.#head;
            while (current.next) {
                current = current.next;
            }
            current.next = newNode;
        }

        this.#size++;
    }

    prepend(value) {
        const newNode = new Node(value);
        newNode.next = this.#head;
        this.#head = newNode;
        this.#size++;
    }

    find(value) {
        let current = this.#head;
        while (current) {
            if (current.value === value) {
                return current;
            }
            current = current.next;
        }
        return null;
    }

    remove(value) {
        if (!this.#head) return false;

        if (this.#head.value === value) {
            this.#head = this.#head.next;
            this.#size--;
            return true;
        }

        let current = this.#head;
        while (current.next) {
            if (current.next.value === value) {
                current.next = current.next.next;
                this.#size--;
                return true;
            }
            current = current.next;
        }

        return false;
    }

    toArray() {
        const result = [];
        let current = this.#head;
        while (current) {
            result.push(current.value);
            current = current.next;
        }
        return result;
    }

    get size() {
        return this.#size;
    }

    toString() {
        const parts = this.toArray().map(String);
        parts.push("null");
        return parts.join(" -> ");
    }
}

// test
const list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.prepend(0);
console.log(list.toString()); // "0 -> 1 -> 2 -> 3 -> null"
console.log(list.size);       // 4

list.remove(2);
console.log(list.toString()); // "0 -> 1 -> 3 -> null"

console.log(list.find(3).value); // 3
console.log(list.find(99));      // null

תרגיל 5 - מערכת הרשאות עם ירושה

class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }

    login() {
        console.log(`${this.name} logged in`);
    }

    getPermissions() {
        return ["read"];
    }

    describe() {
        return `User: ${this.name} (${this.email})`;
    }
}

class Moderator extends User {
    constructor(name, email, department) {
        super(name, email);
        this.department = department;
    }

    getPermissions() {
        return ["read", "edit", "delete"];
    }

    ban(user) {
        console.log(`${this.name} banned ${user.name}`);
    }

    describe() {
        return `${super.describe()} - Moderator [${this.department}]`;
    }
}

class Admin extends Moderator {
    constructor(name, email) {
        super(name, email, "All");
    }

    getPermissions() {
        return ["read", "edit", "delete", "admin"];
    }

    createUser(name, email) {
        console.log(`${this.name} created user ${name}`);
        return new User(name, email);
    }

    static superAdmin(email) {
        return new Admin("Super Admin", email);
    }
}

// test
const user = new User("Alice", "alice@mail.com");
const mod = new Moderator("Bob", "bob@mail.com", "Support");
const admin = Admin.superAdmin("admin@mail.com");

console.log(user.getPermissions());  // ["read"]
console.log(mod.getPermissions());   // ["read", "edit", "delete"]
console.log(admin.getPermissions()); // ["read", "edit", "delete", "admin"]

console.log(mod.describe());
// "User: Bob (bob@mail.com) - Moderator [Support]"

mod.ban(user);  // "Bob banned Alice"
admin.createUser("Charlie", "charlie@mail.com");
// "Super Admin created user Charlie"

console.log(admin instanceof User);      // true
console.log(admin instanceof Moderator); // true
console.log(admin instanceof Admin);     // true

תרגיל 6 - מחלקה Iterable

class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const end = this.end;
        const step = this.step;

        return {
            next() {
                if (current <= end) {
                    const value = current;
                    current += step;
                    return { value, done: false };
                }
                return { done: true };
            }
        };
    }

    includes(n) {
        if (n < this.start || n > this.end) return false;
        return (n - this.start) % this.step === 0;
    }

    toArray() {
        return [...this];
    }

    get length() {
        return Math.max(0, Math.floor((this.end - this.start) / this.step) + 1);
    }
}

// test
const range = new Range(1, 10, 2);

for (const n of range) {
    console.log(n); // 1, 3, 5, 7, 9
}

console.log(range.includes(5)); // true
console.log(range.includes(4)); // false
console.log(range.toArray());   // [1, 3, 5, 7, 9]
console.log(range.length);      // 5
console.log([...range]);        // [1, 3, 5, 7, 9]

[Symbol.iterator]() מחזיר iterator - אובייקט עם מתודת next() שמחזירה { value, done }. זה מה שמאפשר ל-for...of ולספרד (...) לעבוד.


תרגיל 7 - תבנית Observer

class EventEmitter {
    #events = {};

    on(event, listener) {
        if (!this.#events[event]) {
            this.#events[event] = [];
        }
        this.#events[event].push(listener);
    }

    off(event, listener) {
        if (!this.#events[event]) return;
        this.#events[event] = this.#events[event].filter(l => l !== listener);
    }

    emit(event, ...data) {
        if (!this.#events[event]) return;
        this.#events[event].forEach(listener => listener(...data));
    }
}

class Store extends EventEmitter {
    #state;

    constructor(initialState = {}) {
        super();
        this.#state = { ...initialState };
    }

    getState() {
        return { ...this.#state };
    }

    setState(updates) {
        this.#state = { ...this.#state, ...updates };
        this.emit("change", this.getState());
    }

    subscribe(listener) {
        this.on("change", listener);

        // return unsubscribe function
        return () => {
            this.off("change", listener);
        };
    }
}

// test
const store = new Store({ count: 0, name: "App" });

const unsubscribe = store.subscribe((state) => {
    console.log("State changed:", state);
});

store.setState({ count: 1 });
// "State changed: { count: 1, name: "App" }"

store.setState({ count: 2, name: "Updated" });
// "State changed: { count: 2, name: "Updated" }"

unsubscribe();
store.setState({ count: 3 }); // no output - unsubscribed

console.log(store.getState()); // { count: 3, name: "Updated" }

הפונקציה subscribe מחזירה closure שזוכר את ה-listener וקורא ל-off כדי להסיר אותו. זו תבנית נפוצה מאוד ב-React ובספריות state management.