الوحدة 3 · العمل مع البيانات
القيم المفقودة وMap وSet
17 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تقرأ البيانات التي قد تكون مفقودة بأمان باستخدام السلسلة الاختيارية (optional chaining) وعامل الـ nullish coalescing (??)، وتختار Map للبحث والعدّ وSet للقيم الفريدة.
- 1الخطأ الذي ستراه أكثر من غيره
- 2السلسلة الاختيارية: ?.
- 3القيم الافتراضية بـ ??
- 4Map: البحث بأي مفتاح
- 5Set: كل قيمة مرة واحدة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
البيانات الحقيقية فيها ثغرات. بعض المصاريف لها ملاحظة وبعضها لا. وبعضها أُرفق به إيصال. والإعداد الذي لم يغيّره المستخدم قط لا يُحفظ أصلًا. على Pocket أن يتعامل مع كل هذا دون أن ينهار، ودون أن يخلط بين “لا شيء” وقيمة حقيقية مثل الصفر.
الخطأ الذي ستراه أكثر من غيره
قراءة خاصية غير موجودة في كائن أمر لا بأس به؛ فتحصل على undefined. أما قراءة خاصية من undefined فليست كذلك:
const expense = { label: "Coffee", amount: 450 };
console.log(expense.receipt); // undefined: fine
try {
console.log(expense.receipt.url); // reading .url of undefined
} catch (error) {
console.log(error.message);
}TypeError: Cannot read properties of undefined (reading 'url') ربما يكون الخطأ الأكثر شيوعًا في JavaScript كلها. (الـ try/catch هنا موجودة فقط كي يستمر المثال في العمل فتقرأ الرسالة؛ والقسم 5 يشرحها كما ينبغي.) والرسالة تخبرك بالضبط بما حدث: شيء ما قبل .url كان undefined.
وقبل أن تمدّ يدك إلى الحل، تذكّر الفرق بين القيمتين “الفارغتين”. undefined تعني عادةً “لم يُضبط قط”: خاصية مفقودة، أو معامل لم يمرّره أحد، أو متغيّر بلا قيمة. أما null فتُضبط عن قصد لتعني “فارغ عمدًا”. قد يخزّن Pocket القيمة receipt: null لمصروف اختار المستخدم ألّا يرفق به إيصالًا.
السلسلة الاختيارية: ?.
العامل ?. يقرأ الخاصية فقط إن لم يكن ما على يساره null أو undefined. فإن كان كذلك، يتوقّف التعبير كله ويعطي undefined بدلًا من أن يرمي خطأ:
const withReceipt = { label: "Train", receipt: { url: "/r/88.png" } };
const withoutReceipt = { label: "Coffee" };
console.log(withReceipt.receipt?.url);
console.log(withoutReceipt.receipt?.url);
console.log(withoutReceipt.tags?.[0]); // works with brackets
console.log(withoutReceipt.format?.()); // and with callsضع ?. مباشرة بعد الجزء الذي قد يكون مفقودًا. ففي expense.receipt?.url أنت تقول: “expense يجب أن يكون موجودًا، أما receipt فقد لا يكون”.
القيم الافتراضية بـ ??
?. تعطيك undefined، وفي العادة تريد قيمة بديلة. تعرّفت على || للقيم الافتراضية في القسم 2، ومعها عيبها: إنها تلجأ إلى البديل مع كل قيمة falsy، بما في ذلك 0 و"". أما عامل nullish coalescing ?? (أي “البديل عند غياب القيمة”) فلا يلجأ إلى البديل إلا مع null وundefined:
const settings = { dailyLimit: 0, nickname: "" };
console.log(settings.dailyLimit || 5000); // 5000: the user's 0 is lost
console.log(settings.dailyLimit ?? 5000); // 0: kept
console.log(settings.nickname ?? "friend"); // "": kept
console.log(settings.currency ?? "USD"); // missing: default usedاستخدم ?? للقيم الافتراضية. ولا تستخدم || إلا حين تريد فعلًا استبدال النصوص الفارغة والأصفار أيضًا. ويجتمع العاملان مع ?. بشكل طبيعي:
const expense = { label: "Coffee" };
const receiptUrl = expense.receipt?.url ?? "No receipt";
console.log(receiptUrl);وهناك أيضًا صيغة إسناد، settings.currency ??= "USD"، تضبط الخاصية فقط إن كانت حاليًا null أو undefined.
Map: البحث بأي مفتاح
في الدرس السابق استخدمت كائنًا عاديًا لحساب مجاميع الفئات. الكائنات تعمل جيدًا مع السجلّات ذات أسماء الخصائص المعروفة. أما المجموعة التي تكبر مع البيانات، ومفاتيحها قيم لا تعرفها مسبقًا، فلها في JavaScript بنية مخصّصة: Map:
const counts = new Map();
for (const category of ["food", "travel", "food", "fun", "food"]) {
counts.set(category, (counts.get(category) ?? 0) + 1);
}
console.log(counts.get("food"), counts.has("rent"), counts.size);
for (const [category, count] of counts) {
console.log(`${category}: ${count}`);
}set(key, value) تخزّن، وget(key) تقرأ (أو تعطي undefined)، وhas(key) تفحص، وdelete(key) تحذف، وsize تعدّ المدخلات. وحلقة for...of تعطيك أزواج [key, value] بترتيب إضافتها الأول.
لماذا لا تستخدم كائنًا دائمًا؟ الـ Map تقبل أي قيمة مفتاحًا، بما في ذلك الأرقام والكائنات، بينما يحوّل الكائن كل مفتاح إلى نص. ولها size حقيقية. وليست لها خصائص موروثة تتعثّر بها: فالكائن الذي مفاتيحه نصوص يكتبها المستخدم قد يتصادم مع أسماء مدمجة مثل constructor. والخيار الافتراضي الجيد: الكائنات للسجلّات ذات الحقول الثابتة، والـ Maps لجداول البحث المبنية من البيانات.
Set: كل قيمة مرة واحدة
الـ Set مجموعة تظهر فيها كل قيمة مرة واحدة على الأكثر. وإضافة قيمة موجودة أصلًا لا تفعل شيئًا:
const categories = ["food", "travel", "food", "fun", "travel"];
const unique = new Set(categories);
console.log(unique.size, unique.has("fun"));
unique.add("food"); // already there: no change
console.log([...unique]); // back to an array, in first-seen order[...new Set(list)] هي الطريقة المعتمدة في سطر واحد لإزالة التكرار من مصفوفة. وhas أسرع بكثير من includes على مصفوفة كبيرة، لأن الـ Set لا تبحث عنصرًا عنصرًا. وقد أضافت الإصدارات الحديثة من JavaScript عمليات على المجموعات مثل union وintersection، وهي متاحة في كل المتصفّحات الحالية.
بهذا تكتمل أدوات Pocket للتعامل مع البيانات: المصفوفات والكائنات والـ Maps والـ Sets، وعوامل التعامل مع القيم المفقودة. وفي القسم 4، يذهب كل ذلك إلى صفحة ويب حقيقية.
أهم النقاط
- قراءة خاصية من
undefinedأوnullترمي TypeError؛ أما?.فتتوقّف وتُرجعundefinedبدلًا من ذلك. ??توفّر قيمة افتراضية فقط معnullوundefined، فتنجو القيم الحقيقية مثل0و""، على عكس||.- استخدم
?.فقط حيث تكون البيانات اختيارية فعلًا؛ فنثرها في كل مكان يُخفي أخطاء حقيقية. - الـ Map تخزّن أزواجًا من المفاتيح والقيم بمفاتيح من أي نوع، وتحفظ ترتيب الإضافة، ولها
getوsetوhasوsize. - الـ Set تخزّن كل قيمة مرة واحدة على الأكثر، لذلك تُزيل
[...new Set(list)]التكرار.