שלושה טריקים של foreach שכבר לא כדאי לעשות

15/05/2022

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

1. קודם כל הקוד

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

var each = require('foreach');

each([1,2,3], function (value, key, array) {
    // value === 1, 2, 3
    // key === 0, 1, 2
    // array === [1, 2, 3]
});

each({0:1,1:2,2:3}, function (value, key, object) {
    // value === 1, 2, 3
    // key === 0, 1, 2
    // object === {0:1,1:2,2:3}
});

למרות שאני לא ממש רואה את הערך בכזאת ספריה, הרבה כותבי ספריות אחרים ב npm כן ראו את הערך ובאתר יש 141 ספריות שתלויות ב foreach כאשר הגירסה הקודמת (לפני פרשיית ההשתלטות המזויפת) הגיעה כמעט ל 6 מיליון הורדות סך הכל ב 8 השנים מאז פורסמה. צריך להגיד, הספריה די נטושה ועם JavaScript מודרני אין ממש טעם להשתמש בה. הקוד הבא עובד בכל הדפדפנים ונותן לולאה זהה למערכים ואוביקטים:

const x = [10, 20, 30, 40];

const y = { a: 10, b: 20, c: 30, d: 40 };

function handler(value, key) {
  console.log(`value ${value}; key ${key}`);
}

for (const [k, v] of Object.entries(x)) {
  handler(v, k);
}

console.log('---');

for (const [k, v] of Object.entries(y)) {
  handler(v, k);
}

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


var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString;

module.exports = function forEach (obj, fn, ctx) {
    if (toString.call(fn) !== '[object Function]') {
        throw new TypeError('iterator must be a function');
    }
    var l = obj.length;
    if (l === +l) {
        for (var i = 0; i < l; i++) {
            fn.call(ctx, obj[i], i, obj);
        }
    } else {
        for (var k in obj) {
            if (hasOwn.call(obj, k)) {
                fn.call(ctx, obj[k], k, obj);
            }
        }
    }
};

2. טריק 1 - בדיקה אם אוביקט הוא פונקציה

הטריק הראשון בפונקציה הוא הבדיקה האם הפרמטר השני הוא פונקציה. הם משתמשים בבלוק הבא:

if (toString.call(fn) !== '[object Function]') {
    throw new TypeError('iterator must be a function');
}

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

if (typeof(fn) !== 'function') {}

או יותר טוב לכתוב את הקוד בתוך try/catch ולזהות TypeError. הסיבה להמרה ל String היא כדי לא לתפוס פונקציות מערכת מסוימות ולהכריח את הקורא להעביר פונקציה שהוא הגדיר לבד. הבעיה עם זה היא שהיום יש הרבה פונקציות אמיתיות שמשתמש מגדיר לבד שיחזירו טקסט אחר ב toString שלהן, לדוגמה פונקציות אסינכרוניות:

> async function h() { }
undefined
> Object.prototype.toString.call(h)
'[object AsyncFunction]'

3. טריק 2 - בדיקה אם אוביקט הוא מערך

החלק הבא של הקוד צריך להפעיל לולאה אחרת על אוביקט ולולאה אחרת על מערך. הם בודקים מה הם קיבלו באמצעות המאפיין length - אם יש לך length אתה מערך, בלעדיו אתה אוביקט. כאן אנחנו כבר לא יכולים להשתמש ב typeof כי:

> typeof [1,2,3]
'object'

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

> const x = { a: 10, length: 20 };
>  var l = x.length;
> l === +l
true

במקום זה, הדרך המקובלת היום לבדוק אם אוביקט מסוים הוא מערך היא הפונקציה Array.isArray שמובנית בשפה ונתמכת בכל מקום:

> const x = { a: 10, length: 20 };
> Array.isArray(x)
false

4. טריק 3 - בדיקה אם שדה מסוים שייך לאוביקט או ל Prototype שלו

המנגנון המיותר השלישי והאחרון הוא הבדיקה:

if (Object.prototype.hasOwnProperty(k)) {}

הפונקציה hasOwnProperty נועדה להבדיל בין מאפיינים שהם "באמת" באוביקט שלי, לבין מאפיינים שאפשר להגיע אליהם מהאוביקט שלי אבל הם בעצם מוגדרים על אוביקט אחר שהוא במעלה ה Prototype Chain. לדוגמה המאפיין toString זמין על כל אוביקט JavaScript:

> const x = { a: 10 }
> x.toString()
'[object Object]'

אבל לא היינו רוצים לקבל אותו באיטרציה של forEach כי הוא מוגדר ב Object.prototype ולא על האוביקט שעליו אני רץ. הטריק הזה עדיין רלוונטי אם אתם מבצעים לולאת for ... in על אוביקט, אבל מה שהופך אותו למיושן זה שכמעט אין סיבה לביצוע לולאות כאלה. הלולאה שהראיתי בתחילת הפוסט:

for (const [k, v] of Object.entries(x)) {
  handler(v, k);
}

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