במאמר הקודם למדנו על קבוע, במאמר הזה אנחנו נדבר על אחד מסימני ההיכר החזקים של ECMAScript 6 – הגדרת משתנה עם let.
על מנת להבין למה זה חשוב, אני אדבר בתחילה על scope. מה זה בכלל? סביר להניח שאתם יודעים, לפחות בתחום תת ההכרה כי זה אחד מהפיצ'רים הבולטים ב-JavaScript. גם כתבתי על זה מאמר מפורט. אבל אני אסביר שוב. בואו ונסתכל על הקוד הבא:
"use strict";
var x = 5;
function moshe () {
var x = 123;
console.log(x); //Will be 123
}
console.log(x); //Will be 5
moshe();
console.log(x); //Will be 5
אם נפעיל את הקוד הבא (ואתם יכולים להעתיק ולהדביק אותו בקונסולה של כלי המפתחים), יהיה אפשר לראות שהקונסולה תראה לנו
5
123
5
למה? כי לפונקציה moshe יש סקופ משלה שבה המשתנה X נפרד לחלוטין ממשתנה X הגלובלי. בשניה שאנחנו מגדירים משתנה עם var בתוך פונקציה, אנחנו יוצרים משתנה פרטי בסקופ שאיתו אפשר לעבוד אבל הוא לא רלוונטי ל'עולם החיצוני' שמחוץ לפונקציה.
מה הבעיה? שהיה אפשר להגדיר scope רק לפונקציות ולא לבלוקים אחרים כמו למשל משפטי תנאי.
בואו ונדגים עם הקוד הבא:
"use strict";
var x = 5;
if (true) {
var x = 123;
console.log(x); //Will be 123
}
console.log(x); //Will be 123
מה קורה פה? אנחנו מגדירים var בתוך ה-if, ל-if היה אמור להיות סקופ משלו. אבל אין כזה דבר ב-ECMAScript 5. לא יכולנו להגדיר scope משלנו מתי שבא לנו אלא רק בתוך פונקציה.
בדיוק בשביל זה יש לנו את ECMAScript 6 ואת let שמאפשר לנו להגדיר סקופ איפה שבא לנו. אפילו במשפט תנאי:
"use strict";
var x = 5;
if (true) {
let x = 123;
console.log(x); //Will be 123
}
console.log(x); //Will be 5
אפשר להריץ את זה בקונסולה או לפתוח את הקונסולה שלכם ולראות את זה רץ ב-codepen:
See the Pen ECMAScript 6 – Let by Ran Bar-Zik (@barzik) on CodePen.
על מנת להמחיש את הבעיה, אני אדגים באמצעות בעיה שאני מאוד אוהב לתת במבחנים טכניים – בעיית ה-scope כשנוצרת כאשר מצמידים אירועים לאלמנטים. בואו ונניח שיש לי כמה כפתורים בדף ואני רוצה שבכל לחיצה עליהם אני אראה בקונסולה את המספר הסידורי של הכפתור שנלחץ. כדי לעשות את זה, אני כותב את הקוד הבא:
var buttons = document.getElementsByTagName('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('You clicked button #' + i);
});
}
הי הי הי! לא להתעלף פה! הקוד הזה די פשוט. מה שהוא עושה זה להשתמש בשאילתת JS פשוטה על מנת להכניס את כל אלמנטי ה-DOM שהם כפתורים אל מערך buttons. באמצעות לולאות for פשוטה אני מצמיד אירוע לחיצה לכל כפתור שמדפיס לקונסולה את המספר הסידורי של כל כפתור.
מה הבעיה? שועלי JS ותיקים כבר ידעו ישר. אבל למי שלא, העזרו ב-codepen הבא:
See the Pen ECMAScript 5 Scope failure by Ran Bar-Zik (@barzik) on CodePen.
לחיצה על הכפתור תראה לנו שבקונסולה אנחנו רואים כל פעם
"You clicked button #3"
בהנחה שיש שלושה כפתורים. אבל רגע, למה? למה אני לא רואה כל פעם את המספר של הכפתור שנלחץ? בדיוק בגלל בעית ה-scope! מחוץ ל-addEvent ה-i קבוע, אבל בתוך ההצמדה של האירוע ההתיחסות ל-i היא גלובלית. זה קשה להבנה באופן אינטואיטיבי, אבל כל מתכנת JS מספיק מנוסה נתקל בבעיה הזו.
יש כמה דרכים לפתור את זה, כולן דוחות למדי, למשל דרך אחת היא:
var buttons = document.getElementsByTagName('button');
for (var i = 0; i < buttons.length; i++) {
bindEvent(i);
}
function bindEvent(k) {
var button = buttons[k]
button.addEventListener('click', function() {
console.log(k);
});
}
See the Pen ECMAScript 5 Scope failure – solution by Ran Bar-Zik (@barzik) on CodePen.
מה שקורה פה הוא שאנחנו בעצם מכריחים את JS ליצור סקופ חדש באמצעות הגדרת פונקציה. ואז ה-i שמשתמשים בו הוא מקומי ולא גלובלי. די פשוט (אם אתם יודעים את הפתרון) אבל מסובך למדי אם לא מבינים עד הסוף את עניין ה-scope או שאתם באמצע ראיון עבודה מלחיץ או שיש דד-ליין.
ברגע שיש הבנה בנוגע לסקופ עם ECMAScript 6 הפתרון הוא פשוט, באמצעות let נאכוף את הסקןפ החדש בקלות רבה, בלי להשתמש בפונקציות מלאכותיות בכוח על מנת ליצור סקופ חדש:
var buttons = document.getElementsByTagName('button');
for (var i = 0; i < buttons.length; i++) {
let j = i;
buttons[j].addEventListener('click', function() {
console.log('You clicked button #' + j);
});
}
אתם מוזמנים לבחון את הפתרון כאן:
See the Pen ECMAScript 5 Scope failure solution by ES6 by Ran Bar-Zik (@barzik) on CodePen.
ההמלצה שלי? להתחיל להשתמש בזה, אין פלא שבכל פעם שאנחנו רואים הגדרה של משתנה ב-JavaScript יותר ויותר אנשים משתמשים ב-let על מנת להמנע מבלבול של ה-scope. יש לא מעט המלצות לוותר על ה-var לחלוטין.
2 תגובות
Greate example.
Obviously the classic use should be inside the for loop itself:
`for (let i = 0; i < buttons.length; i++)`…
אני הייתי משתמשת ב map, האמת, אבל אז זה לא ידגים את מה שרצית להסביר 🙂