משמעות מילת המפתח this ב-javascript

שייך לקטגוריות JavaScript

הגדרה – this הוא האובייקט שקרא לפונקציה

מילת המפתח this שמורה לכל scope (כלומר, פונקציה או scope גלובאלי) ובכל scope יש לה ערך אחר. ערך ה-this, לפי ההגדרה בעצם נקבע לפי האובייקט שקרא לפונקציה.

מה זה בעצם scope? כל פונקציה מקבלת מרחב משלה להגדיר בו משתנים ומאפיינים. כשאנחנו רוצים לגשת למשתנה כלשהו אנחנו צריכים לעשות זאת מהמרחב בו הוא הוגדר. ב-javascript, כאשר איננו מגדירים פונקציה, הכל קורה ב-scope הגלובאלי, כלומר ה-window.

קוד

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};
o.method();

הגדרנו אובייקט חדש בשם o והגדרנו לו מאפיין מסוג פונקציה שמבצע alert ל-this.name.
אך מהיכן this מגיע?
בהפעלת הפונקציה, o.method(), נקבל את הערך "moon". התהליך שקורה כאן הוא כזה:

  1. יצרנו משתנה מסוג מחרוזת בעל הערך "moon" ושמנו אותו במאפיין name של האובייקט o.
  2. יצרנו פונקציה עם תוכן מסויים ושמנו אותה כמאפיין ל-o.
  3. הפעלנו את הפונקציה כאשר o הוא זה שקורא לה – הוא זה שמופיע לפני הנקודה.
  4. הפונקציה מופעלת כאשר this הוא בעצם רפרנס ל-o. נקבל o.name שערכו הוא "moon".

חשוב להבין! פונקציה היא משתנה לכל דבר. היא מכילה ביטויים שאנחנו יכולים להפעיל מאוחר יותר. אין באמת שיוך בין הפונקציה שלנו ל-o בכך שהגדרנו אותה כמאפיין של o. השיוך האמיתי יקרה רק בקריאה לפונקציה שאז this יתורגם ל-o.

מכך, אפשר להתחכם קצת:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
x.method=o.method;
x.method();

מה עשינו כאן? יצרנו אובייקט חדש בשם x עם מאפיין name בעל הערך sun.

את הרפרנס לפונקציה שהגדרנו ושמנו ב-o שמנו גם ב-x.method. בהפעלה של x.method לא נקבל, חס וחלילה, את moon, כי אין שום שיוך ממשי ל-o. נקבל את sun כי x הוא זה שקרא לפונקציה, וערכו של this נקבע לפי מי שקרא לפונקציה.

אפשר לומר שלכל פונקציה ב-javascript, נוסף ארגומנט בלתי נראה ששמו this. ובנוסף על כך, בכל קריאה לפונקציה, נשלח הארגומנט הזה מאחורי הקלעים עם הערך של אותו אובייקט שקרא לפונקציה.

function f() { alert(this.name); }

הופכת ל

function f(this) { alert(this.name); }

כלומר:

var o={}; // declaring new object
o.name="moon";
o.method=function (this) {
	alert(this.name);
};
o.method(o);
 
var x={};
x.name="sun";
x.method=o.method;
x.method(x);

ב-C++, הקומפיילר מוסיף לכל מתודה במחלקה ארגומנט ראשון נוסף בשם this שהוא אובייקט מסוג המחלקה, ובעצם כל הפעלה של מתודה הופכת לקריאה למתודה בצירוף המשתנה.

class Foo {
	string name="foo";
	string GetName() { return this.name; }
}

Foo o=new o();
string s=o.GetName();

בעצם הופך להיות מעין:

class Foo {
	string name="foo";
	string GetName(Foo this) { return this.name; }
}

Foo o=new Foo();
string s=o.GetName(o);

שינוי ערכו של this

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

javascript נותנת לנו כלים לקרוא לפונקציה עם this אחר מבלי לשבור את הראש יותר מדי, בעזרת המתודות call/apply. שתי המתודות דומות: הן מתודות של פונקציה (יושבות ב-Function.prototype) שמקבלות אובייקט וארגומנטים, וקוראות לפונקציה עצמה עם הארגומנטים. השונה הוא דרך שליחת הארגומנטים – call מקבל אותם כארגומנטים רגילים (כמו params של C#) ואילו apply מקבל אותם כמערך.

את הקוד העליון, נשנה כדי שיפעל עם call:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
o.method.call(x); 
מה שיקרה פה, מאחורי הקלעים:
x.temporaryMethod=o.method;
x.temporaryMethod();

האופרטור new ו-this

פונקציה ב-javascript היא לא רק מספר statements להפעלה מאוחר יותר, היא גם מתפקדת כמחלקה. כל פונקציה היא גם מחלקה. ממחלקה אפשר ליצור מופעים.

function Foo(name) {
	this.name=name;
}

הפונקציה הזו, כמו כל הפונקציות, משמשת גם כמחלקה, ומופע שלה אנו יוצרים כך:

var instance=new Foo("bar");
alert(instance.name); // "bar"

מה בעצם קרה כאן מאחורי הקלעים?בעת הפעלת new Foo("bar")

var instance={}; // declaring new object
Foo.call(instance,name); // calling the constructor with the new object as this, and the argument "name"

הפעלת הבנאי תפעיל את הביטוי:

this.name=name; // this is the "instance" object. assign name as the sent argument

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

this ו-prototype

המאפיין prototype הינו מאפיין של מחלקות, ובכל השמה של ערך בו – הוא זמין לכל המופעים של המחלקה. בעת גישה למאפיין אצל אובייקט, קודם ייבדק האם יש לאובייקט הזה את המאפיין, ואם לא, ייבדק ה-prototype של המחלקה ממנה האובייקט נוצר (כאשר Object היא המחלקה הבסיסית).

ב-javascript מספר מחלקות מוגדרות מראש:
Object
Function
String
Number
Array
Date
Error
RegExp

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

ב-C#3.0 יש פיצ'ר חדש שנקרא extension method, שמרחיב בעצם פונקציונליות של מחלקות מוגדרות כמו string, int וכו'

ב-ruby גם כן הפיצ'ר קיים.
String.prototype.trim=function () {
	return this.replace(/^\s+|\s+$/g,"");
};
var s="    x        ";
alert("<"+s+">");
alert("<"+s.trim()+">");

לאחר הרחבת ה-prototype של המחלקה String במאפיין בשם trim שמכיל פונקציה שמקצצת רווחים מתחילת וסוף המחרוזת, נוכל לגשת מתוך כל מחרוזת למתודה הזו. this בתוך הפונקציה בעצם יכיל את הערך של המחרוזת, כי משתנה המחרוזת הוא זה שקרא לפונקציה.

DOM ו-this

<div id="div" onclick="alert(this.innerHTML);">div</div>

ה-DOM ידע לפרש את קוד ה-HTML לקוד הבא:

div.onclick=function () {
	alert(this.innerHTML);
};

כאן זה כבר נראה לנו מוכר, onclick מוגדר כמאפיין שערכו הוא פונקציה עם שימוש ב-this.

הדפדפן יודע לשים "מאזין" ללחיצת עכבר, ובלחיצה הוא בודק האם המיקום של העכבר נמצא על אותו div. אם כן, הוא בודק האם ל-div יש מאפיין בשם onclick, ואם כן הוא פשוט מפעיל את הפונקציה בצורה כזו:

div.onclick();

כך, this יקבל רפרנס ל-div.

לסיכום

this הוא עולם ומלואו ב-javascript. הבנה מעמיקה שלו נותנת את האופציה לקצר בקוד ולכתוב בצורה יעילה ומתקדמת.

איפוס CSS

שייך לקטגוריות CSS

באופן טבעי, לכל דפדפן הגדרות ברירת מחדל שונות ל-CSS. הריווחים בין תגיות p, ריווחים מקצוות המסך, גדלי פונטים לתגיות ה-header.
כרגיל, אין תקן, ובחלק מהמקרים, אנחנו המפתחים נתקלים בקשיים ובסוף מגלים שהתמונה למטה נדחפת כי בין כל אחת מ-6 הפסקאות שמעליה ב-IE יש רווח גדול יותר מאשר ב-FF.

YUI, ספריית הקליינט סיד של yahoo, מכילה קובץ שמבצע Reset לכל התכונות האלו. להלן הקוד עם תוספות מיניאטוריות:

html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
	margin:0;
	padding:0;
}
table {
	border-collapse:collapse;
	border-spacing:0;
}
fieldset,img {
	border:0;
}
address,caption,cite,code,dfn,em,strong,th,var {
	font-style:normal;
	font-weight:normal;
}
ol,ul,li {
	list-style:none;
	list-style-type:none;
}
caption,th {
	text-align:left;
}
h1,h2,h3,h4,h5,h6,th,td {
	font-size:100%;
	font-weight:normal;
}
q:before,q:after {
	content:'';
}
abbr,acronym {
	border:0;
}

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

יאהו נותנים לבצע link לקובץ מתוך השרתים שלהם:

<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.2.2/build/reset-fonts-grids/reset-fonts-grids.css" />

אירועי DOM בתחביר דמוי #C

שייך לקטגוריות JavaScript
btn.Click+=new EventHandler(btn_Click);
btn.Click+=new EventHandler(btn_Click2);

btn.Click-=new EventHandler(btn_Click);

page.Load+=new EventHandler(page_Load);

מכירים את הסינטקס? כך מצרפים/מורידים אירוע למאזין ב-#C. ניתן ליישם סינטקס זה גם ב-JavaScript בצד לקוח.

בעת כתיבת הסקריפט נתקלתי בכמה קשיים. += היא פעולה המחברת בין מספרים ומשרשרת מחרוזות. שלא כמו ב-#C, אי אפשר לגרום לה לעשות משהו אחר. מכאן נובע, שחייבים להשתמש במספרים או מחרוזות כדי לסכם את כל ה-EventHanlerים המשוייכים לאלמנט. החלטתי על מספרים.

מכיוון שמחלקות מחזירות את עצמן בביטוי ה-new, יש למצוא שיטה להחזיר מספר. השיטה היא שכתוב מתודת valueOf. מתודת ה-valueOf הינה מתודה פנימית שמופעלת בכל פעם שקוראים לערך של אובייקט, כמו למשל בפעולת חיבור, חיסור וכו' (כמו מתודת toString שנקראת אוטומטית בשרשור מחרוזות). הערך שהמתודה תחזיר הוא הערך שיוחזר מביטוי ה-new אל תוך פעולת החיבור או החיסור.

הערך עצמו צריך להיות מזהה ייחודי. השתמשתי בשיטה "בינארית" הבנויה מקומבינציות של חזקות של 2.

קצת על השיטה הבינארית

השיטה מאפשרת למספר פריטים בחזקות של 2 – 1, 2, 4, 8, 16 וכו'. כל חיבור של מספרים אלו ייתן קומבינציה אחת בלבד.
דוגמה:
13 = 8+4+1
26 = 16+8+2
כל חזקה של 2 מייצגת "ביט" במיקום ספציפי.
הביט הראשון הוא 0 והמספר המייצג אותו הוא 1 (2 בחזקת 0)
הביט השני הוא 1 והמספר המייצג אותו הוא 2 (2 בחזקת 1)
הביט השלישי הוא 2 והמספר המייצג אותו הוא 4 (2 בחזקת 2)
וכן הלאה…
כאשר ביט אינו פעיל, הוא אינו נמצא בקומבינציה. כמו ש-2 לא נמצא בקומבינציה של 13.
כדי לבדוק האם ביט "דלוק" בתוך מספר אחר, משתמשים באופרטור & המחזיר את אותו מספר אם הוא דלוק ו-0 אם לא.

בכל פעולת חיבור, הרפרנס לפונקציה מצטרף למערך של כל ה-handlers ומודבק אליו אותו אינדקס בחזקת 2. האינדקס נקבע עפ"י מספר ה-handlers.

לאלמנט שאליו רוצים להוסיף/להוריד events מצמידים מאפיין, נניח Click, המאותחל ל-0. כל += של EventHandler יוסיף לתוך המאפיין Click מספר כלשהו שהוא חזקה של 2. חיבור של כמה EventHandlerים לתוך Click ייתן מספר שפירוק שלו ירכיב קומבינציה אחת בלבד של חזקות של 2.

בנוסף למאפיין, נשייך לאלמנט event בעזרת attachEvent/addEventListener שבהפעלתו נרוץ על כל מערך ה-handlers, ובמידה והמאפיין המספרי מכיל את המזהה של ה-handler מהמערך, נפעיל את הפונקציה.

הסינטקס הסופי הוא כזה:

Event.Register(element1,"Click");
element1.Click+=new EventHandler(handler1); // valueOf returns 1. add 1
element1.Click+=new EventHandler(handler2); // valueOf returns 2. add 2
element1.Click-=new EventHandler(handler2); // valueOf returns 2. subtract 2
element1.Click+=new EventHandler(handler3); // valueOf returns 4. add 4

לאחר כל שורות הקוד, ערכו של המאפיין Click הינו 5. 5, לאחר פירוק בינארי, הופך ל-4+1, שמייצגים "ביטים" 0 ו-2. לכן, הפונקציות באינדקס 0 ו-2 יקראו.

לחיצה על האלמנט תרוץ על מערך ה-handlers ותפעיל את פונקציות 0 ו-2 כי הביטים שלהן דלוקים במאפיין Click.

הקוד במלואו:

<script type="text/javascript">
//<![CDATA[
function EventHandler(handler) {
	this.Handler=handler;
	this._index=EventHandler._allHandlers.length;

	EventHandler._allHandlers.push(this);
}
EventHandler.prototype.valueOf=function () {
	// if function is already in the list, return its value
	for (var i=0;i<EventHandler._allHandlers.length;i++) {
		handler=EventHandler._allHandlers[i];
		if (handler.Handler==this.Handler && handler.registered) return Math.pow(2,i);
	}

	this.registered=true;

	return Math.pow(2,this._index);
};
EventHandler._allHandlers=[];

if (typeof(Event)=="undefined") Event={};
Event.Register=function (element,name) {
	element[name]=0;

	var fire=function (e) { Event.Fire(element[name],e); };

	if (element.attachEvent) element.attachEvent("on"+name.toLowerCase(),fire);
	else element.addEventListener(name.toLowerCase(),fire,false);
};
Event.Fire=function (handlers,e) {
	var idx=0;

	// cache power function for better performance
	var pow=Math.pow;

	for (var i=0;i<EventHandler._allHandlers.length;i++) {
		var idx=pow(2,i);
		// if the current index in the power of 2 contained in the handles list, call the function
		if ((handlers & idx)>0) EventHandler._allHandlers[i].Handler(e);
	}
}

function handler1() { alert(1); }
function handler2() { alert(2); }
function handler3() { alert(3); }
function handler4() { alert(4); }

onload=function () {
	var element1=document.getElementById("element1");
	Event.Register(element1,"Click");
	element1.Click+=new EventHandler(handler1);
	element1.Click+=new EventHandler(handler2);
	element1.Click-=new EventHandler(handler2);
	element1.Click+=new EventHandler(handler3);

	var element2=document.getElementById("element2");
	Event.Register(element2,"Click");
	element2.Click+=new EventHandler(handler1);
	element2.Click+=new EventHandler(handler2);
};
//]]>
</script>
<div id="element1">element1</div>
<div id="element2">element2</div>

באגים נפוצים ב-IE6

שייך לקטגוריות CSS, HTML, JavaScript, דפדפנים

דפדפן Interner Explorer בגרסה 6 היה שיפור עצום מבחינת תמיכה בתקנים מאז ימי 5 הזוהרים (ואף מילה על 5.5). ה-CSS התחיל להתנהג בצורה תקנית בעזרת DOCTYPE מתאים וגם ה-dom התחיל להיות תואם להגדרות W3C.

עם זאת, הרבה פעמים אנו נתקלים בבאגים מרגיזים שיכולים לעכב את העבודה שלנו כדי למצוא workarounds. מה לעשות ש-IE7 ו-Firefox עדיין לא השתלטו על השוק, ו-IE6 תופס נתח לא קטן, ואנחנו כמתכנתי web צריכים להתאים את האתרים שלנו לכל הדפדפנים הנפוצים.

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

#1 : ערכי margin כפולים ב-float

לכל אלמנט שחל עליו float כלשהו, ערכי ה-margin שיחולו עליו יוכפלו. אין הסבר הגיוני לזה.

דוגמה:

<div style="float:left;margin-left:100px;">IE6 doubles the margin and renders margin-left with 200px</div>

באג margin כפול ב-float

פתרון:

הצורה הכי אלגנטית לפתור את הבאג הזה היא לתת מאפיין display:inline לאלמנט בעל ה-float. הגדרת ה-float בכל מקרה משכתבת את ה-display ל-block. זה פותר את הבעיה, ולא מונע מהאלמנט להמשיך לתפקד כמו block.

#2 : תזוזה של פיקסל כאשר רוחב/גובה אי זוגיים

כאשר ממקמים אבסולטית אלמנט בתוך אלמנט אחר (בעל position:relative) ורוחבו או גובהו של אלמנט האב אי זוגיים, האלמנט הפנימי יזוז פיקסל אחד אחורה

<div style="width:199px;height:99px;position:relative;border:1px solid #000;">
	<div style="width:100px;position:absolute;background-color:red;right:0;bottom:0;"></div>
</div>
באג ריווח של פיקסל אחדניתן לראות כי יש רווח של פיקסל מימין
ומלמטה

פתרון:

לבאג זה לא מצאתי ממש פתרון קסם, אך בעזרת ה-underscore hack ניתן למשוך את האלמנט למיקום הנכון שלו:

<div style="width:199px;height:99px;position:relative;border:1px solid #000;">
	<div style="width:100px;position:absolute;background-color:red;right:0;bottom:0;_margin-bottom:-1px;_margin-right:-1px;"></div>
</div>

#3 : תמונות וצבעי רקע נחתכים

את מי שלא מכיר דרכי פתרון, הבאג הזה יכול להוציא מהכלים. קצת הסבר כללי – ב-css של IE יש מאפיין readonly שנקרא hasLayout. לאלמנט "יש" layout אם באחד המאפיינים הבאים יש ערך שונה מברירת המחדל: width, height, float, zoom, או אחד מהמאפיינים הבאים: display:inline-block, position:absolute, writing-mode:tb-rl.

הבאג עצמו חל על אלמנטים ללא layout וגורם להתנהגויות מוזרות: צבעי ותמונות רקע חתוכים או לא מופיעים, טקסט שנעלם ומופיע בסימון, טקסט שמיקומו נדחף.

הפתרון, כאמור, הוא לתת אחד מהמאפיינים שלמעלה. מכיוון שכל המאפיינים משפיעים על צורתו ומיקומו של האלמנט ולא תמיד זה מה שנרצה, נוכל להשתמש במאפיין zoom עם ערך של 1. ה-zoom לא באמת ישתנה, אך האלמנט יקבל layout והבאג ייפתר.

ul li {
	background-image:url('image.gif');
	zoom:1;
}

#4 : opacity, טקסט ו-ClearType (גם ב-IE7)

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

בדוגמה אפשר לראות טקסט בתוך div עם שקיפות של 40%. הטקסט המודגש נראה מגורען ולא חלק.

פתרון:

צבע רקע כלשהו לאלמנט המכיל פותר את הבאג.

#5 : תיבת select עולה על אלמנטים

ללא התחשבות ב-z-index, תיבות בחירה ב-IE6 עולות על כל אלמנט (כמעט).

אם יש לי תפריט שנפתח במעבר עכבר, ומתחת לאיזור בו אמור להפתח התפריט יש select, התיבה תופיע מעל אותו אלמנט.

פתרון:

אלמנט אחד ש-select לא עולה עליו הוא iframe. אבל אלמנטים אחרים כן יכולים לעלות על iframe. המסקנה המתבקשת היא – הצגת iframe מעל ה-select והצגת האלמנט מעל אותו iframe. את ה-iframe אפשר לעשות שקוף בעזרת opacity של 0. את המיקום והגודל של ה-iframe יש להעתיק מהאלמנט אותו אנו חפצים לשים מעל ה-select.

> html
<iframe id="hider-iframe" style="display:none" frameborder="0" scrolling="no"></iframe>

> css
#hider-iframe {
	position:absolute;
	filter:alpha(opacity=0);
}

> js
function ShowMenu() {
	HiderIframeShow(menuElement);
	menuElement.style.display="";
}
function HiderIframeShow(clone) {
	var hiderIframe=document.getElementById("hider-iframe");
	hiderIframe.style.top=clone.style.top;
	hiderIframe.style.left=clone.style.left;
	hiderIframe.style.width=clone.offsetWidth+"px";
	hiderIframe.style.height=clone.offsetHeight+"px";
	hiderIframe.style.display="";
}
אם משתמשים בספריית prototype, הפונקציה Position.clone תעתיק את המיקום המדוייק.

מאפייני ה-top/left מועתקים מערכי האלמנט, בהנחה שגם ה-iframe וגם האלמנט בעלי position:absolute ויושבים תחת אותו offsetParent (הא הראשון בעל position:absolute/relative או ה-body). אם לא כך הדבר – יש להשתמש במנגנון המחשב מיקום מדוייק של האלמנט.

#6 : צבעים של PNG שקוף משתנים

נתקלתי לאחרונה בתופעה מוזרה: כאשר משתמשים ב-AlphaImageLoader filter של IE6 להצגת תמונות PNG עם שקיפות, התמונות התמונות מוצגות כהות יותר ממה שהן במקור. כן, IE6 ממש משנה ערכי RGB על התמונה.

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

לאחר מחקר קטן גיליתי שאכן יש באג במידה וה-PNG יוצאה מפוטושופ (כנראה גם בעורכי תמונות אחרים). מצאתי כלי חינמי המתקן את העוולה -

TweakPNG. לאחר ההפעלה של התוכנה פותחים את קובץ ה-PNG, מוחקים את ה-Chunk בשם gAMA, ושומרים.

(מיותר לציין, Firefox ו-IE7, בעזרת תגית img או background-image, עומדים בגבורה במשימה ומציגים צבעים נכונים מראש).

underscore hack

בהתקלות בכל באג נוסף שמצריך טיפול מיוחד רק ב-IE6 אפשר להשתמש ב-underscore hack: כל מאפיין css שיוצמד לו מקף תחתון לפני שמו – יוחל רק ב-IE6.

.className {
	background-color:red;
	_background-color:blue;
}

בכל דפדפן מלבד IE6 נראה צבע אדום, אך IE6 כן יתייחס ל-_background-color כמאפיין חוקי, ולכן צבע הרקע יהיה כחול.

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

מידע נוסף

מידע נוסף ניתן למצוא בקישור באגים ב-CSS של IE6.


קטגוריות

חיפוש

עיקבו אחרי (אקספרימנטלי!)