לדלג לתוכן

4.7 לולאת האירועים ותכנות פונקציונלי תרגול

תרגול - לולאת האירועים ותכנות פונקציונלי

תרגיל 1 - סדר הפלט

בלי להריץ את הקוד, כתבו מה יהיה סדר הפלט בכל אחד מהמקרים הבאים. הסבירו למה.

א.

console.log("1");

setTimeout(() => {
    console.log("2");
}, 0);

Promise.resolve().then(() => {
    console.log("3");
});

queueMicrotask(() => {
    console.log("4");
});

console.log("5");

ב.

async function first() {
    console.log("A");
    await second();
    console.log("B");
}

async function second() {
    console.log("C");
    await Promise.resolve();
    console.log("D");
}

console.log("E");
first();
console.log("F");

ג.

setTimeout(() => console.log("1"), 0);

Promise.resolve()
    .then(() => {
        console.log("2");
        return Promise.resolve();
    })
    .then(() => {
        console.log("3");
        setTimeout(() => console.log("4"), 0);
    });

Promise.resolve().then(() => {
    console.log("5");
});

setTimeout(() => console.log("6"), 0);

console.log("7");

תרגיל 2 - חסימה ופתרון

הקוד הבא מחשב מספרים ראשוניים אבל חוסם את ה-UI:

function findPrimes(limit) {
    const primes = [];
    for (let i = 2; i <= limit; i++) {
        let isPrime = true;
        for (let j = 2; j <= Math.sqrt(i); j++) {
            if (i % j === 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) {
            primes.push(i);
        }
    }
    return primes;
}

// this blocks the UI for seconds
const primes = findPrimes(5000000);

שכתבו את הפונקציה כך שהיא לא תחסום את ה-UI:
- פצלו את העבודה לחלקים (chunks) באמצעות setTimeout
- הוסיפו קולבק של התקדמות שמתעדכן כל חלק
- הוסיפו קולבק של סיום שמחזיר את התוצאה

findPrimesAsync(5000000, {
    chunkSize: 10000,
    onProgress(current, total) {
        console.log(`${Math.round(current / total * 100)}%`);
    },
    onComplete(primes) {
        console.log(`Found ${primes.length} primes`);
    }
});

תרגיל 3 - פונקציות טהורות

בכל אחד מהמקרים הבאים, קבעו אם הפונקציה טהורה או לא, והסבירו למה. אם היא לא טהורה, שכתבו אותה כך שתהיה טהורה.

// a
function discount(price) {
    return price * 0.9;
}

// b
let count = 0;
function increment() {
    return ++count;
}

// c
function getFullName(user) {
    return `${user.firstName} ${user.lastName}`;
}

// d
function addItem(cart, item) {
    cart.push(item);
    return cart;
}

// e
function getRandomGreeting(name) {
    const greetings = ["Hello", "Hi", "Hey"];
    const index = Math.floor(Math.random() * greetings.length);
    return `${greetings[index]}, ${name}!`;
}

// f
function formatDate(date) {
    return new Date(date).toLocaleDateString("he-IL");
}

תרגיל 4 - אי-שינוי

כתבו את הפונקציות הבאות בגישת אי-שינוי (immutable). אסור לשנות את האובייקטים המקוריים.

א. toggleTodo - מקבלת מערך של todos ו-id, ומחזירה מערך חדש שבו ה-todo המתאים הפוך (completed הופך מ-true ל-false ולהפך).

const todos = [
    { id: 1, text: "Learn JS", completed: false },
    { id: 2, text: "Learn React", completed: false },
    { id: 3, text: "Build project", completed: true }
];

const result = toggleTodo(todos, 2);
// result[1].completed === true
// todos[1].completed === false (original unchanged!)

ב. updateNestedProperty - מקבלת אובייקט מקונן, מערך של מפתחות (path), וערך חדש. מחזירה אובייקט חדש עם הערך המעודכן.

const state = {
    user: {
        name: "Alice",
        address: {
            city: "Tel Aviv",
            street: "Rothschild"
        }
    },
    settings: {
        theme: "dark"
    }
};

const newState = updateNestedProperty(state, ["user", "address", "city"], "Jerusalem");
// newState.user.address.city === "Jerusalem"
// state.user.address.city === "Tel Aviv" (original unchanged!)
// newState.settings === state.settings (same reference - not copied unnecessarily)

ג. groupBy - מקבלת מערך ומפתח, ומחזירה אובייקט שבו הפריטים מקובצים לפי הערך של המפתח.

const people = [
    { name: "Alice", department: "Engineering" },
    { name: "Bob", department: "Marketing" },
    { name: "Charlie", department: "Engineering" },
    { name: "Dana", department: "Marketing" },
    { name: "Eve", department: "Sales" }
];

const grouped = groupBy(people, "department");
// {
//     Engineering: [{ name: "Alice", ... }, { name: "Charlie", ... }],
//     Marketing: [{ name: "Bob", ... }, { name: "Dana", ... }],
//     Sales: [{ name: "Eve", ... }]
// }

תרגיל 5 - הרכבה ו-pipe

א. כתבו פונקציית pipe שמקבלת כמות בלתי מוגבלת של פונקציות ומחזירה פונקציה חדשה שמפעילה אותן בסדר (משמאל לימין).

ב. השתמשו ב-pipe כדי לבנות את הפונקציות הבאות:

  • processUsername - מקבלת מחרוזת, חותכת רווחים, הופכת לאותיות קטנות, מחליפה רווחים ב-underscore, ומסירה תווים שאינם אותיות, ספרות או underscore.
  • calculatePrice - מקבלת מחיר, מחיל הנחה של 10%, מוסיף מע"מ של 17%, ומעגל לשתי ספרות אחרי הנקודה.
processUsername("  Hello World 123!  "); // "hello_world_123"
calculatePrice(100); // 105.30

ג. כתבו פונקציית pipeAsync שעובדת כמו pipe אבל תומכת בפונקציות אסינכרוניות (שמחזירות Promise).

const processUser = pipeAsync(
    async (id) => {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
        return response.json();
    },
    (user) => ({ ...user, name: user.name.toUpperCase() }),
    (user) => `User: ${user.name} (${user.email})`
);

const result = await processUser(1);
// "User: LEANNE GRAHAM (Sincere@april.biz)"

תרגיל 6 - קרינג - Currying

א. כתבו פונקציית curry כללית שמקבלת פונקציה ומחזירה גרסה curried שלה.

function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);

curriedAdd(1)(2)(3);    // 6
curriedAdd(1, 2)(3);    // 6
curriedAdd(1)(2, 3);    // 6
curriedAdd(1, 2, 3);    // 6

ב. השתמשו ב-currying כדי לבנות מערכת validators:

// curried validator builders
const minLength = curry((min, fieldName, value) => {
    // returns null if valid, error message if invalid
});

const maxLength = curry((max, fieldName, value) => {
    // ...
});

const matches = curry((regex, message, fieldName, value) => {
    // ...
});

const required = curry((fieldName, value) => {
    // ...
});

// compose validators for a field
const validateUsername = [
    required("username"),
    minLength(3, "username"),
    maxLength(20, "username"),
    matches(/^[a-zA-Z0-9_]+$/, "only letters, numbers, and underscores", "username")
];

const validateEmail = [
    required("email"),
    matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "invalid email format", "email")
];

// validate function that runs all validators
function validate(validators, value) {
    // returns array of error messages (empty if valid)
}

validate(validateUsername, "");           // ["username is required"]
validate(validateUsername, "ab");         // ["username must be at least 3 characters"]
validate(validateUsername, "hello world");// ["username: only letters, numbers, and underscores"]
validate(validateUsername, "alice_123");  // []
validate(validateEmail, "not-an-email"); // ["email: invalid email format"]

תרגיל 7 - מיקרו-משימות בפועל

כתבו פונקציה scheduler שמנהלת תור משימות עם עדיפויות:

  • scheduler.addTask(fn, priority) - מוסיפה משימה עם עדיפות ("high", "normal", "low")
  • משימות high רצות כמיקרו-משימות (queueMicrotask)
  • משימות normal רצות עם setTimeout(fn, 0)
  • משימות low רצות עם requestAnimationFrame (אם זמין) או setTimeout(fn, 100)
  • scheduler.run() - מריצה את כל המשימות בתור
const scheduler = createScheduler();

scheduler.addTask(() => console.log("Normal 1"), "normal");
scheduler.addTask(() => console.log("High 1"), "high");
scheduler.addTask(() => console.log("Low 1"), "low");
scheduler.addTask(() => console.log("High 2"), "high");
scheduler.addTask(() => console.log("Normal 2"), "normal");

scheduler.run();

// expected order:
// High 1
// High 2
// Normal 1
// Normal 2
// Low 1