EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 3 · العمل مع البيانات

القيم المفقودة وMap وSet

17 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تقرأ البيانات التي قد تكون مفقودة بأمان باستخدام السلسلة الاختيارية (optional chaining) وعامل الـ nullish coalescing (??)، وتختار Map للبحث والعدّ وSet للقيم الفريدة.

  1. 1الخطأ الذي ستراه أكثر من غيره
  2. 2السلسلة الاختيارية: ?.
  3. 3القيم الافتراضية بـ ??
  4. 4Map: البحث بأي مفتاح
  5. 5Set: كل قيمة مرة واحدة

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

البيانات الحقيقية فيها ثغرات. بعض المصاريف لها ملاحظة وبعضها لا. وبعضها أُرفق به إيصال. والإعداد الذي لم يغيّره المستخدم قط لا يُحفظ أصلًا. على Pocket أن يتعامل مع كل هذا دون أن ينهار، ودون أن يخلط بين “لا شيء” وقيمة حقيقية مثل الصفر.

الخطأ الذي ستراه أكثر من غيره

قراءة خاصية غير موجودة في كائن أمر لا بأس به؛ فتحصل على undefined. أما قراءة خاصية من undefined فليست كذلك:

JavaScript
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 بدلًا من أن يرمي خطأ:

JavaScript
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:

JavaScript
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

استخدم ?? للقيم الافتراضية. ولا تستخدم || إلا حين تريد فعلًا استبدال النصوص الفارغة والأصفار أيضًا. ويجتمع العاملان مع ?. بشكل طبيعي:

JavaScript
const expense = { label: "Coffee" };
const receiptUrl = expense.receipt?.url ?? "No receipt";
console.log(receiptUrl);

وهناك أيضًا صيغة إسناد، settings.currency ??= "USD"، تضبط الخاصية فقط إن كانت حاليًا null أو undefined.

Map: البحث بأي مفتاح

في الدرس السابق استخدمت كائنًا عاديًا لحساب مجاميع الفئات. الكائنات تعمل جيدًا مع السجلّات ذات أسماء الخصائص المعروفة. أما المجموعة التي تكبر مع البيانات، ومفاتيحها قيم لا تعرفها مسبقًا، فلها في JavaScript بنية مخصّصة: Map:

JavaScript
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 مجموعة تظهر فيها كل قيمة مرة واحدة على الأكثر. وإضافة قيمة موجودة أصلًا لا تفعل شيئًا:

JavaScript
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)] التكرار.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

قد يكون للمصروف كائن receipt وقد لا يكون. أيّ تعبير يقرأ url الإيصال دون أن يرمي خطأ أبدًا؟

السؤال 2 من 4

نسبة البقشيش المحفوظة لدى مستخدم هي 0. ماذا يعطي const tip = saved.tip ?? 15;؟

السؤال 3 من 4

تحتاج إلى عدّ المصاريف في كل فئة، ثم لاحقًا عرض الفئات بترتيب ظهورها الأول. أيّها الأنسب؟

السؤال 4 من 4

ماذا تُرجع new Set(["food", "travel", "food", "fun"]).size؟

تدريب

بيئة تجريب الكود

تعامل مع البيانات المفقودة بأمان

بعض بيانات Pocket ناقصة. أكمل أربع دوال:

  • receiptLabel(expense) تُرجع url الإيصال، أو "No receipt" حين تكون receipt مفقودة أو null.
  • dailyLimit(settings) تُرجع settings.dailyLimit، أو 5000 حين لا يوجد حدّ. وقد يكون settings نفسه undefined، ويجب الإبقاء على حدّ محفوظ قيمته 0.
  • countByCategory(expenses) تُرجع Map من الفئة إلى عدد المصاريف فيها. والمصاريف التي لا فئة لها تُعدّ ضمن "other".
  • uniqueCategories(expenses) تُرجع مصفوفة فيها كل فئة مرة واحدة، بترتيب ظهورها الأول (والمفقودة على أنها "other").
const expenses = [
  { label: "Coffee", amount: 450, category: "food" },
  { label: "Train", amount: 1220, category: "travel", receipt: { url: "/r/88.png" } },
  { label: "Lunch", amount: 1350, category: "food", receipt: null },
  { label: "Gift", amount: 5000 },
];

// The receipt's url, or "No receipt" (receipt may be missing or null)
function receiptLabel(expense) {
  return expense.receipt.url;
}

// settings may be undefined; a saved limit of 0 is a real choice
function dailyLimit(settings) {
  return settings.dailyLimit || 5000;
}

// A Map from category to how many expenses have it;
// expenses without a category count as "other"
function countByCategory(expenses) {
}

// Each category once, in the order first seen (missing ones as "other")
function uniqueCategories(expenses) {
  return expenses.map((e) => e.category);
}

console.log(receiptLabel(expenses[1]), dailyLimit({ dailyLimit: 0 }));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • receiptLabel تُرجع الـ url أو “No receipt”
  • dailyLimit تحتفظ بالصفر، وقيمتها الافتراضية 5000، وتنجو من غياب الإعدادات
  • countByCategory تُرجع Map بالأعداد، مع “other” للفئات المفقودة
  • uniqueCategories تعرض كل فئة مرة واحدة، بترتيب ظهورها الأول