לדלג לתוכן

4.2 מילת המפתח this הרצאה

מילת המפתח this

ב-JS, this היא מילת מפתח מיוחדה שמצביעה על ה"הקשר" שבו פונקציה רצה. בניגוד לפייתון, שבה self הוא פשוט הפרמטר הראשון של מתודה, ב-JS הערך של this נקבע לפי הדרך שבה הפונקציה נקראת, לא לפי המקום שבו היא מוגדרת.

זה אחד הנושאים הכי מבלבלים ב-JS, אבל הכללים עצמם פשוטים.


this במתודה של אובייקט

כשפונקציה נקראת כמתודה של אובייקט (עם נקודה), this מצביע על האובייקט:

const user = {
    name: "Alice",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

user.greet(); // "Hello, I'm Alice"
// this === user

זה מתנהג כמו self בפייתון:

# class User:
#     def __init__(self, name):
#         self.name = name
#     def greet(self):
#         print(f"Hello, I'm {self.name}")

ההבדל: בפייתון self מועבר אוטומטית כפרמטר ותמיד מצביע על האובייקט. ב-JS, this נקבע לפי מי שקרא לפונקציה.


this בפונקציה רגילה

כשפונקציה נקראת לבד (לא כמתודה), this הוא:
- ב-strict mode: undefined
- בלי strict mode: האובייקט הגלובלי (window בדפדפן, global ב-Node.js)

"use strict";

function showThis() {
    console.log(this);
}

showThis(); // undefined (strict mode)
// without strict mode
function showThis() {
    console.log(this === window); // true in browser
}

showThis();

בפייתון אין מקבילה לזה - פונקציה רגילה לא מכירה את self בכלל, רק מתודות של מחלקות.


this בפונקציית חץ - arrow function

לפונקציות חץ אין this משלהן. הן יורשות את this מהסקופ החיצוני (lexical this):

const user = {
    name: "Alice",
    greetRegular() {
        // regular function - this is the object
        console.log(`Regular: ${this.name}`);
    },
    greetArrow: () => {
        // arrow function - this is inherited from outer scope (global/module)
        console.log(`Arrow: ${this.name}`);
    }
};

user.greetRegular(); // "Regular: Alice"
user.greetArrow();   // "Arrow: undefined"

למה arrow function שימושית? כשרוצים לשמר this בתוך callback:

const user = {
    name: "Alice",
    friends: ["Bob", "Charlie"],

    showFriends() {
        // 'this' here is user

        // BUG: regular function callback loses 'this'
        this.friends.forEach(function(friend) {
            console.log(`${this.name} knows ${friend}`);
            // this.name is undefined! 'this' is not user anymore
        });

        // FIX: arrow function inherits 'this' from showFriends
        this.friends.forEach((friend) => {
            console.log(`${this.name} knows ${friend}`);
            // this.name is "Alice" - arrow keeps the outer 'this'
        });
    }
};

user.showFriends();

הכלל: השתמשו ב-arrow functions כשאתם רוצים ש-this יישאר כמו ב-scope החיצוני (למשל ב-callbacks). השתמשו בפונקציות רגילות למתודות של אובייקטים.


this ב-event handler

כשפונקציה משמשת כ-event handler, this מצביע על ה-element שקיבל את האירוע:

const button = document.querySelector("#myButton");

button.addEventListener("click", function() {
    console.log(this); // the button element
    this.style.backgroundColor = "red";
});

// with arrow function - this is NOT the button
button.addEventListener("click", () => {
    console.log(this); // window or undefined (inherited from outer scope)
    // this.style.backgroundColor = "red"; // ERROR!
});

אם רוצים את ה-element ב-arrow function, אפשר להשתמש בפרמטר event:

button.addEventListener("click", (event) => {
    console.log(event.target);       // the clicked element
    console.log(event.currentTarget); // the element the listener is on
    event.currentTarget.style.backgroundColor = "red";
});

this במחלקה - class

בתוך class, this מצביע על המופע (instance) - בדיוק כמו self בפייתון:

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

    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
}

const alice = new User("Alice");
alice.greet(); // "Hello, I'm Alice"
// this === alice

אבל יש מלכודת - אם מוציאים מתודה מהאובייקט, this הולך לאיבוד:

const alice = new User("Alice");
const greet = alice.greet; // extracting the method

greet(); // "Hello, I'm undefined"
// 'this' is no longer alice!

נראה איך לפתור את זה בהמשך עם bind.


איבוד הקשר - losing context

זו הבעיה הנפוצה ביותר עם this - כשמעבירים מתודה כ-callback, ה-this הולך לאיבוד:

class Timer {
    constructor() {
        this.seconds = 0;
    }

    start() {
        // BUG: 'this' is lost inside setTimeout callback
        setInterval(function() {
            this.seconds++; // TypeError: Cannot read property 'seconds' of undefined
            console.log(this.seconds);
        }, 1000);
    }
}

const timer = new Timer();
timer.start(); // ERROR!

שלושה פתרונות:

פתרון 1 - arrow function (הכי נפוץ)

class Timer {
    constructor() {
        this.seconds = 0;
    }

    start() {
        setInterval(() => {
            this.seconds++; // arrow inherits 'this' from start()
            console.log(this.seconds);
        }, 1000);
    }
}

פתרון 2 - bind

class Timer {
    constructor() {
        this.seconds = 0;
    }

    tick() {
        this.seconds++;
        console.log(this.seconds);
    }

    start() {
        setInterval(this.tick.bind(this), 1000);
    }
}

פתרון 3 - שמירה במשתנה (ישן, פחות מומלץ)

class Timer {
    constructor() {
        this.seconds = 0;
    }

    start() {
        const self = this; // save reference
        setInterval(function() {
            self.seconds++;
            console.log(self.seconds);
        }, 1000);
    }
}

bind, call, apply

שלוש מתודות שמאפשרות לקבוע את this באופן ידני.

bind - יוצרת פונקציה חדשה עם this קבוע

const user = {
    name: "Alice",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

const greet = user.greet;
greet(); // "Hello, I'm undefined"

const boundGreet = user.greet.bind(user);
boundGreet(); // "Hello, I'm Alice"

// bind can also pre-fill arguments (partial application)
function multiply(a, b) {
    return a * b;
}

const double = multiply.bind(null, 2);
console.log(double(5));  // 10
console.log(double(10)); // 20

bind שימושי במיוחד עם event handlers ו-callbacks:

class App {
    constructor() {
        this.count = 0;
        // bind the method in the constructor
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.count++;
        console.log(`Clicked ${this.count} times`);
    }

    init() {
        document.querySelector("#btn").addEventListener("click", this.handleClick);
    }
}

call - קוראת לפונקציה עם this מסוים (מיידית)

function greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const alice = { name: "Alice" };
const bob = { name: "Bob" };

greet.call(alice, "Hello", "!"); // "Hello, I'm Alice!"
greet.call(bob, "Hi", ".");     // "Hi, I'm Bob."

apply - כמו call, אבל הארגומנטים במערך

greet.apply(alice, ["Hello", "!"]); // "Hello, I'm Alice!"

// useful when you have arguments in an array
const args = ["Hey", "!!!"];
greet.apply(bob, args); // "Hey, I'm Bob!!!"

// modern alternative with spread:
greet.call(bob, ...args); // same thing

ההבדלים בקצרה:
- bind - מחזירה פונקציה חדשה, לא קוראת מיד
- call - קוראת מיד, ארגומנטים בנפרד
- apply - קוראת מיד, ארגומנטים במערך

כלל זיכרון: bind = build new function, call = comma separated args, apply = array args


סיכום הכללים

מצב ערך של this
מתודה: obj.method() obj
פונקציה רגילה: func() undefined (strict) / window
פונקציית חץ this מהסקופ החיצוני
event handler (רגיל) ה-element שקיבל את האירוע
new Constructor() האובייקט החדש שנוצר
func.call(obj) / func.apply(obj) obj
func.bind(obj)() obj

כללי אצבע:
- מתודות באובייקט - השתמשו בפונקציות רגילות (לא arrow)
- callbacks בתוך מתודות - השתמשו ב-arrow functions
- event handlers - פונקציה רגילה אם רוצים this כ-element, או arrow עם event.target
- מחלקות - bind ב-constructor או arrow functions כ-class fields


השוואה לפייתון

נושא פייתון ג׳אווהסקריפט
הפניה עצמית self - פרמטר מפורש this - מילת מפתח מובנית
קביעת ערך תמיד האובייקט תלוי בדרך הקריאה
איבוד הקשר לא קורה (self מועבר מפורשות) קורה כשמעבירים מתודה כ-callback
קישור ידני לא נדרש bind / call / apply
arrow functions אין מקבילה יורשות this מהסקופ

בפייתון, self תמיד מצביע על האובייקט כי הוא מועבר כפרמטר ראשון. ב-JS, this דינמי ויכול להשתנות בהתאם לדרך שבה הפונקציה נקראת. זו הסיבה שלמדנו כלים כמו bind ו-arrow functions - הם פותרים בעיות שפשוט לא קיימות בפייתון.