לדלג לתוכן

4.1 קלוז׳רים וסקופ פתרון

פתרון - קלוז׳רים וסקופ


תרגיל 1 - חידות scope chain

חידה א:

let x = 1;

function a() {
    let x = 2;

    function b() {
        console.log(x);
    }

    return b;
}

const fn = a();
fn(); // 2

תשובה: 2 - הפונקציה b רואה את x מהסקופ שבו היא הוגדרה (בתוך a), לא מהסקופ הגלובלי. זה סקופ לקסיקלי.

חידה ב:

let count = 0;

function increment() {
    count++;
    console.log(count);
}

function reset() {
    let count = 0; // this is a NEW local variable, not the global one
    console.log(count);
}

increment(); // 1
increment(); // 2
reset();     // 0 (local count, doesn't affect the global one)
increment(); // 3 (global count continues from 2)

תשובה: 1, 2, 0, 3 - הפונקציה reset יוצרת משתנה count חדש ומקומי - היא לא משנה את הגלובלי.

חידה ג:

function outer() {
    let x = 10;

    function middle() {
        let y = 20;

        function inner() {
            let z = 30;
            console.log(x + y + z);
        }

        x = 100; // modifies x before inner() runs
        inner();
    }

    middle();
}

outer(); // 150

תשובה: 150 - כי x שונה ל-100 לפני שנקראה inner. ה-closure שומר הפניה למשתנה, לא עותק של הערך. אז 100 + 20 + 30 = 150.


תרגיל 2 - מונה עם closure

function createCounter(initialValue = 0) {
    let value = initialValue;

    return {
        increment() {
            return ++value;
        },

        decrement() {
            return --value;
        },

        getValue() {
            return value;
        },

        reset() {
            value = initialValue;
        }
    };
}

// test
const counter = createCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.getValue());  // 11
counter.reset();
console.log(counter.getValue());  // 10

// can't access value directly
console.log(counter.value); // undefined

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

function memoize(fn) {
    const cache = new Map();

    function memoized(...args) {
        const key = JSON.stringify(args);

        if (cache.has(key)) {
            return cache.get(key);
        }

        const result = fn(...args);
        cache.set(key, result);
        return result;
    }

    // bonus: method to clear the cache
    memoized.cache = {
        clear() {
            cache.clear();
        }
    };

    return memoized;
}

// test
function slowSquare(n) {
    console.log("Computing...");
    return n * n;
}

const fastSquare = memoize(slowSquare);

console.log(fastSquare(5));  // "Computing..." -> 25
console.log(fastSquare(5));  // 25 (from cache)
console.log(fastSquare(3));  // "Computing..." -> 9
console.log(fastSquare(5));  // 25 (from cache)

fastSquare.cache.clear();
console.log(fastSquare(5));  // "Computing..." -> 25 (cache was cleared)

תרגיל 4 - משתנים פרטיים

function createPasswordManager() {
    let password = null;

    return {
        setPassword(newPassword) {
            if (newPassword.length < 8) {
                return false;
            }
            password = newPassword;
            return true;
        },

        checkPassword(attempt) {
            return attempt === password;
        },

        getHint() {
            if (!password) {
                return "No password set";
            }
            return password.slice(0, 2) + "*".repeat(password.length - 2);
        }
    };
}

// test
const pm = createPasswordManager();
console.log(pm.setPassword("abc"));            // false
console.log(pm.setPassword("mySecret123"));    // true
console.log(pm.checkPassword("wrong"));        // false
console.log(pm.checkPassword("mySecret123"));  // true
console.log(pm.getHint());                     // "my*********"

// password is not accessible
console.log(pm.password); // undefined

תרגיל 5 - closure בלולאה

חלק א - זיהוי הבאג

const buttons = [];

for (var i = 0; i < 5; i++) {
    buttons.push({
        label: `Button ${i}`,
        click: function() {
            console.log(`Clicked button ${i}`);
        }
    });
}

buttons[0].click(); // "Clicked button 5"
buttons[2].click(); // "Clicked button 5"
buttons[4].click(); // "Clicked button 5"

כל הכפתורים מדפיסים "Clicked button 5" כי var הוא function-scoped. יש רק i אחד, וכשהלולאה מסתיימת הוא שווה 5. כל ה-closures מצביעים על אותו i.

שימו לב ש-label נכון (Button 0, Button 1, ...) כי template literal מחושב מיד בזמן ה-push, אבל ה-closure של click לא מופעל עד שלוחצים.

חלק ב - תיקון עם let

const buttons = [];

for (let i = 0; i < 5; i++) {
    buttons.push({
        label: `Button ${i}`,
        click: function() {
            console.log(`Clicked button ${i}`);
        }
    });
}

buttons[0].click(); // "Clicked button 0"
buttons[2].click(); // "Clicked button 2"
buttons[4].click(); // "Clicked button 4"

חלק ב - תיקון עם IIFE

const buttons = [];

for (var i = 0; i < 5; i++) {
    (function(j) {
        buttons.push({
            label: `Button ${j}`,
            click: function() {
                console.log(`Clicked button ${j}`);
            }
        });
    })(i);
}

buttons[0].click(); // "Clicked button 0"
buttons[2].click(); // "Clicked button 2"
buttons[4].click(); // "Clicked button 4"

תרגיל 6 - תבנית המודול - module pattern

const TaskManager = (function() {
    let tasks = [];
    let nextId = 1;

    return {
        addTask(title) {
            const task = {
                id: nextId++,
                title,
                done: false
            };
            tasks.push(task);
            return task;
        },

        completeTask(id) {
            const task = tasks.find(t => t.id === id);
            if (task) {
                task.done = true;
                return true;
            }
            return false;
        },

        removeTask(id) {
            const index = tasks.findIndex(t => t.id === id);
            if (index !== -1) {
                tasks.splice(index, 1);
                return true;
            }
            return false;
        },

        getTasks() {
            return tasks.map(t => ({ ...t }));
        },

        getPending() {
            return tasks
                .filter(t => !t.done)
                .map(t => ({ ...t }));
        },

        getStats() {
            const done = tasks.filter(t => t.done).length;
            return {
                total: tasks.length,
                done,
                pending: tasks.length - done
            };
        }
    };
})();

// test
TaskManager.addTask("Learn closures");
TaskManager.addTask("Practice exercises");
TaskManager.addTask("Build project");

console.log(TaskManager.getStats());
// { total: 3, done: 0, pending: 3 }

TaskManager.completeTask(1);
console.log(TaskManager.getPending().length); // 2
console.log(TaskManager.getStats());
// { total: 3, done: 1, pending: 2 }

TaskManager.removeTask(2);
console.log(TaskManager.getStats());
// { total: 2, done: 1, pending: 1 }

// tasks and nextId are not accessible
console.log(TaskManager.tasks); // undefined

תרגיל 7 - createMultiplier ושרשרת פונקציות

חלק א

function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log(double(10)); // 20

חלק ב

function compose(...fns) {
    return function(value) {
        // apply functions from right to left
        return fns.reduceRight((result, fn) => fn(result), value);
    };
}

const double = createMultiplier(2);
const addOne = (x) => x + 1;
const square = (x) => x * x;

const transform = compose(addOne, double, square);

console.log(transform(3)); // square(3)=9 -> double(9)=18 -> addOne(18)=19
console.log(transform(5)); // square(5)=25 -> double(25)=50 -> addOne(50)=51

ה-closure ב-compose: הפונקציה המוחזרת סוגרת על המערך fns - היא זוכרת אילו פונקציות הועברו ל-compose גם אחרי ש-compose סיימה לרוץ. כל קריאה ל-transform ניגשת ל-fns דרך ה-closure.