לדלג לתוכן

4.2 מילת המפתח this פתרון

פתרון - מילת המפתח this


תרגיל 1 - חידות this

חידה א:

const obj = {
    name: "Object",
    getName: function() {
        return this.name;
    },
    getNameArrow: () => {
        return this.name;
    }
};

console.log(obj.getName());      // "Object"
console.log(obj.getNameArrow()); // undefined
  • getName - פונקציה רגילה, this הוא obj כי נקראה עם obj.getName()
  • getNameArrow - פונקציית חץ, this נקבע לפי הסקופ שבו האובייקט הוגדר (הסקופ הגלובלי), לא לפי מי שקרא לה. ב-strict mode this.name יהיה undefined

חידה ב:

const person = {
    name: "Alice",
    friends: ["Bob", "Charlie"],
    listFriends() {
        this.friends.forEach(function(friend) {
            console.log(`${this.name} is friends with ${friend}`);
        });
    }
};

person.listFriends();
// "undefined is friends with Bob"
// "undefined is friends with Charlie"

ה-callback הרגיל שמועבר ל-forEach מאבד את this. בתוך ה-callback, this הוא undefined (strict) או window, לא person.

תיקון: להשתמש ב-arrow function ב-callback.

חידה ג:

function Dog(name) {
    this.name = name;
    this.bark = function() {
        console.log(`${this.name} says woof!`);
    };
}

const dog = new Dog("Rex");
const bark = dog.bark;

dog.bark(); // "Rex says woof!"
bark();     // "undefined says woof!" (or TypeError in strict mode)

dog.bark() - נקראה כמתודה, this הוא dog.
bark() - נקראה כפונקציה רגילה, this הולך לאיבוד.


תרגיל 2 - תיקון הקשר שאבד

פתרון עם bind:

class Playlist {
    constructor(name) {
        this.name = name;
        this.songs = [];

        // bind methods in constructor
        this.addSong = this.addSong.bind(this);
        this.play = this.play.bind(this);
    }

    addSong(song) {
        this.songs.push(song);
        console.log(`Added "${song}" to ${this.name}`);
    }

    play() {
        console.log(`Playing ${this.name}:`);
        this.songs.forEach(function(song, i) {
            console.log(`  ${i + 1}. ${song} (from ${this.name})`);
        }.bind(this)); // bind the forEach callback too
    }
}

פתרון עם arrow functions:

class Playlist {
    constructor(name) {
        this.name = name;
        this.songs = [];
    }

    // arrow function as class field - 'this' is always the instance
    addSong = (song) => {
        this.songs.push(song);
        console.log(`Added "${song}" to ${this.name}`);
    };

    play = () => {
        console.log(`Playing ${this.name}:`);
        // arrow callback inherits 'this'
        this.songs.forEach((song, i) => {
            console.log(`  ${i + 1}. ${song} (from ${this.name})`);
        });
    };
}

const playlist = new Playlist("My Favorites");
playlist.addSong("Song A");
playlist.addSong("Song B");
playlist.play();

const addSong = playlist.addSong;
addSong("Song C"); // works

setTimeout(playlist.play, 100); // works

תרגיל 3 - call, apply, bind

חלק א:

function introduce(greeting, hobby) {
    console.log(`${greeting}! I'm ${this.name}, age ${this.age}. I love ${hobby}.`);
}

const alice = { name: "Alice", age: 25 };
const bob = { name: "Bob", age: 30 };

// using call - arguments separated by commas
introduce.call(alice, "Hello", "coding");
// "Hello! I'm Alice, age 25. I love coding."

introduce.call(bob, "Hi", "gaming");
// "Hi! I'm Bob, age 30. I love gaming."

// using apply - arguments in an array
introduce.apply(alice, ["Hey", "reading"]);
// "Hey! I'm Alice, age 25. I love reading."

introduce.apply(bob, ["Yo", "cooking"]);
// "Yo! I'm Bob, age 30. I love cooking."

חלק ב:

function borrowMethod(source, methodName, target) {
    return source[methodName].bind(target);
}

const calculator = {
    value: 0,
    add(n) {
        this.value += n;
        return this;
    },
    getValue() {
        return this.value;
    }
};

const myObj = { value: 100 };

const boundAdd = borrowMethod(calculator, "add", myObj);
boundAdd(5);
boundAdd(10);
console.log(myObj.value); // 115

תרגיל 4 - אובייקט עם שרשור מתודות

class QueryBuilder {
    constructor(table) {
        this.table = table;
        this.fields = ["*"];
        this.conditions = [];
        this.order = null;
        this.limitCount = null;
    }

    select(...fields) {
        this.fields = fields;
        return this;
    }

    where(field, operator, value) {
        const formattedValue = typeof value === "string" ? `'${value}'` : value;
        this.conditions.push(`${field} ${operator} ${formattedValue}`);
        return this;
    }

    orderBy(field) {
        this.order = field;
        return this;
    }

    limit(count) {
        this.limitCount = count;
        return this;
    }

    build() {
        let query = `SELECT ${this.fields.join(", ")} FROM ${this.table}`;

        if (this.conditions.length > 0) {
            query += ` WHERE ${this.conditions.join(" AND ")}`;
        }

        if (this.order) {
            query += ` ORDER BY ${this.order}`;
        }

        if (this.limitCount !== null) {
            query += ` LIMIT ${this.limitCount}`;
        }

        return query;
    }
}

// test
const query = new QueryBuilder("users")
    .select("name", "email")
    .where("age", ">", 18)
    .where("active", "=", true)
    .orderBy("name")
    .limit(10)
    .build();

console.log(query);
// "SELECT name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10"

כל מתודה מחזירה this - מה שמאפשר שרשור. build מחזירה מחרוזת כי היא סוף השרשרת.


תרגיל 5 - this ב-event handlers

class ClickTracker {
    constructor(selector) {
        this.element = document.querySelector(selector);
        this.count = 0;

        // bind the handler so we can remove it later
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.count++;
    }

    start() {
        this.element.addEventListener("click", this.handleClick);
    }

    stop() {
        this.element.removeEventListener("click", this.handleClick);
    }

    getCount() {
        return this.count;
    }

    reset() {
        this.count = 0;
    }
}

// test
const tracker = new ClickTracker("#myButton");
tracker.start();
// click 3 times...
console.log(tracker.getCount()); // 3
tracker.stop();
tracker.reset();
console.log(tracker.getCount()); // 0

נקודות חשובות:
- עושים bind ב-constructor כדי ש-handleClick תמיד תצביע על ה-instance
- שומרים הפניה לאותה פונקציה bound כדי ש-removeEventListener יעבוד (צריך את אותו reference בדיוק)


תרגיל 6 - this במקרים מורכבים

חלק א + ב:

function createEventEmitter() {
    const events = {};

    return {
        on(event, handler) {
            if (!events[event]) {
                events[event] = [];
            }
            events[event].push(handler);
        },

        emit(event, ...args) {
            if (!events[event]) return;

            events[event].forEach((handler) => {
                // use call to set 'this' to the emitter
                handler.call(this, ...args);
            });
        },

        off(event, handler) {
            if (!events[event]) return;
            events[event] = events[event].filter(h => h !== handler);
        },

        once(event, handler) {
            const wrapper = (...args) => {
                handler.call(this, ...args);
                this.off(event, wrapper);
            };
            this.on(event, wrapper);
        }
    };
}

// test
const emitter = createEventEmitter();

function onData(data) {
    console.log(`${this.name} received: ${data}`);
}

emitter.name = "MyEmitter";
emitter.on("data", onData);
emitter.emit("data", "hello"); // "MyEmitter received: hello"

// test once
emitter.once("login", function(user) {
    console.log(`${this.name}: ${user} logged in`);
});

emitter.emit("login", "Alice"); // "MyEmitter: Alice logged in"
emitter.emit("login", "Bob");   // (nothing - handler was removed)

שימו לב ש-emit משתמש ב-handler.call(this, ...args) כדי שה-this בתוך ה-handler יהיה ה-emitter. ב-once, ה-wrapper הוא arrow function כדי שה-this שלו יהיה ה-emitter (ירושה מ-once).


תרגיל 7 - השוואה מסכמת

const obj = {
    value: 42,

    regular() {
        console.log(this.value);         // (A)

        const inner = function() {
            console.log(this.value);     // (B)
        };
        inner();

        const arrow = () => {
            console.log(this.value);     // (C)
        };
        arrow();
    },

    arrow: () => {
        console.log(this.value);         // (D)
    }
};

קריאה obj.regular():

  • (A) - 42 - this הוא obj כי נקראה כמתודה
  • (B) - undefined - inner נקראה כפונקציה רגילה, this הוא undefined (strict) או window
  • (C) - 42 - arrow function יורשת this מ-regular, שם this הוא obj

קריאה obj.arrow():

  • (D) - undefined - arrow function יורשת this מהסקופ שבו האובייקט הוגדר (גלובלי), לא מ-obj

קריאה fn() (כש-fn = obj.regular):

  • (E-A) - undefined - נקראה כפונקציה רגילה, this הולך לאיבוד
  • (E-B) - undefined - פונקציה רגילה בתוך פונקציה רגילה
  • (E-C) - undefined - arrow יורשת this מ-regular, ושם this הוא undefined

קריאה bound() (אחרי bind(obj)):

  • (F-A) - 42 - bind קובע ש-this הוא obj
  • (F-B) - undefined - inner עדיין פונקציה רגילה שנקראת לבד, bind לא משפיע על פונקציות פנימיות
  • (F-C) - 42 - arrow יורשת this מ-regular, ושם this הוא obj (בזכות ה-bind)