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

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

الكائنات والتفكيك (Destructuring) والنشر (Spread)

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

الدرس في لمحة

تمثّل السجلّات الحقيقية على شكل كائنات، وتقرأ خصائصها وتحدّثها، وتفهم أن الكائنات تُتداوَل عبر المرجع، وتنسخها وتحدّثها وتفكّكها باستخدام الـ spread والـ destructuring.

  1. 1الكائنات: قيم مسمّاة مجتمعة
  2. 2المراجع: اسمان وكائن واحد
  3. 3النشر: انسخ وحدّث دون تعديل في المكان
  4. 4التفكيك: فكّ الحزمة إلى متغيّرات

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

حتى الآن كان المصروف في Pocket مجرّد رقم. لكن المصروف الحقيقي له عدّة حقائق مترابطة: ما هو، وكم كلّف، وما فئته، ومتى كان. وحفظها في مصفوفات منفصلة (labels[2] وamounts[2] وcategories[2]) ينجح إلى أن تُرتَّب إحدى المصفوفات دون الأخرى. تحتاج إلى قيمة واحدة تحملها كلها: الكائن (object).

الكائنات: قيم مسمّاة مجتمعة

يُكتب الكائن بأقواس معقوفة ويحتوي على خصائص (properties)، كل منها اسم وقيمة:

JavaScript
const expense = {
  id: "exp-1",
  label: "Coffee",
  amount: 450,
  category: "food",
};

console.log(expense.label);       // dot notation
console.log(expense["amount"]);   // bracket notation
console.log(expense.note);        // missing property: undefined

صيغة النقطة هي الطريقة اليومية لقراءة خاصية. والأقواس المربّعة تؤدّي المهمة نفسها، لكنها تحسب ما بداخلها، وهذا ما تحتاجه حين يكون اسم الخاصية في متغيّر: const field = "category"; expense[field]. وقراءة خاصية غير موجودة تعطي undefined، لا خطأ.

يمكنك إضافة الخصائص وتغييرها وحذفها بعد إنشاء الكائن:

JavaScript
const expense = { label: "Coffee", amount: 450 };
expense.category = "food";   // add
expense.amount = 500;        // change
delete expense.category;     // remove
console.log(expense);
console.log(Object.keys(expense), Object.values(expense));

حين يكون لديك متغيّر يحمل أصلًا الاسم الذي تريده للخاصية، يمكنك كتابته مرة واحدة. { label, amount } اختصار لـ { label: label, amount: amount }، وستراه باستمرار في الكود الحقيقي:

JavaScript
const label = "Bus";
const amount = 270;
const expense = { label, amount, category: "travel" };
console.log(expense);

Object.keys وObject.values تعطيانك مصفوفتين بالأسماء والقيم، وObject.entries تعطيك أزواجًا، لذلك تمرّ for (const [key, value] of Object.entries(expense)) على كل خاصية.

صارت بيانات Pocket الآن مصفوفة من الكائنات، وهي أكثر أشكال البيانات شيوعًا في JavaScript، وكل ما تعلّمته في الدرس السابق ما زال ينطبق:

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, category: "food" },
  { id: "exp-2", label: "Train", amount: 1220, category: "travel" },
  { id: "exp-3", label: "Lunch", amount: 1350, category: "food" },
];
const food = expenses.filter((e) => e.category === "food");
console.log(food.map((e) => e.label));

المراجع: اسمان وكائن واحد

هذا أهم ما يجب أن تفهمه عن الكائنات والمصفوفات. المتغيّر لا يحمل الكائن نفسه؛ بل يحمل مرجعًا (reference) إليه، مثل العنوان. ونسخ المتغيّر ينسخ العنوان:

JavaScript
const original = { label: "Coffee", amount: 450 };
const alias = original;
alias.amount = 999;
console.log(original.amount);       // 999
console.log(alias === original);    // true: the same object
console.log({ a: 1 } === { a: 1 }); // false: two different objects
الاسمان original وalias يشيران كلاهما إلى الكائن نفسه، لذلك يظهر التغيير عبر alias من خلال original. أما الاسم copy فيشير إلى كائن جديد منفصل أُنشئ بالنشر. original alias { label: "Coffee", amount: 999 } copy { label: "Coffee", amount: 450 } كائن واحد كائن جديد
الإسناد ينسخ المرجع؛ والنشر يُنشئ كائنًا جديدًا

=== مع الكائنات تقارن المراجع، لا المحتويات. فالكائنان اللذان يبدوان متطابقين يظلّان كائنين مختلفين.

ولهذا أهمية لأن الدوال تستلم المراجع أيضًا. فإن غيّرت دالة كائنًا أُعطي لها، تغيّر كائن المُستدعي:

JavaScript
function applyDiscount(expense) {
  expense.amount = Math.round(expense.amount * 0.9);
  return expense;
}

const lunch = { label: "Lunch", amount: 1350 };
const discounted = applyDiscount(lunch);
console.log(discounted.amount, lunch.amount); // both 1215

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

النشر: انسخ وحدّث دون تعديل في المكان

صيغة النشر (spread)، أي النقاط الثلاث، تنسخ كل خصائص كائن (أو عناصر مصفوفة) إلى كائن جديد:

JavaScript
const expense = { id: "exp-1", label: "Coffee", amount: 450 };
const updated = { ...expense, amount: 500 };
console.log(expense.amount, updated.amount);

const list = [expense];
const longer = [...list, { id: "exp-2", label: "Bus", amount: 270 }];
console.log(list.length, longer.length);

في { ...expense, amount: 500 } تُنسخ الخصائص بالترتيب، وamount اللاحقة تكتب فوق المنسوخة. فتحصل على كائن جديد، ويبقى الأصل دون مساس. ومع map، هذه هي الطريقة المعتمدة لتغيير عنصر واحد في قائمة دون تعديل أي شيء في مكانه:

JavaScript
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450 },
  { id: "exp-2", label: "Bus", amount: 270 },
];
const fixed = expenses.map((e) => (e.id === "exp-2" ? { ...e, amount: 300 } : e));
console.log(fixed[1].amount, expenses[1].amount);

التفكيك: فكّ الحزمة إلى متغيّرات

التفكيك (destructuring) يسحب الخصائص من كائن إلى متغيّرات تحمل الأسماء نفسها:

JavaScript
const expense = { label: "Train", amount: 1220, category: "travel" };
const { label, amount, note = "no note" } = expense;
console.log(label, amount, note);

const [first, second] = ["food", "travel", "fun"];
console.log(first, second);

= "no note" قيمة افتراضية، تُستخدم حين تكون الخاصية مفقودة أو undefined. ومع المصفوفات، يعتمد التفكيك على الموضع بدلًا من الاسم.

والتفكيك أكثر ما يكون نفعًا في معاملات الدوال، حيث يوثّق بالضبط الخصائص التي تحتاجها الدالة:

JavaScript
function describe({ label, amount, category = "other" }) {
  return `${label}: ${(amount / 100).toFixed(2)} (${category})`;
}
console.log(describe({ label: "Train", amount: 1220 }));

قارنها بالنسخة الخالية من التفكيك، التي تكرّر expense. في كل سطر وتُخفي الخصائص المهمة حتى تقرأ الجسم كله. أما مع التفكيك، فالسطر الأول من الدالة هو عقدها: أعطني شيئًا فيه تسمية ومبلغ، وفئة اختيارية. ويستطيع المُستدعون تمرير كائن مصروف كامل فيه عشر خصائص أخرى؛ والدالة ببساطة تتجاهل الباقي.

وتعمل ... أيضًا بالعكس، بصفتها rest (البقية): const { id, ...details } = expense تضع id في متغيّر خاص به، وكل الخصائص الأخرى في كائن جديد اسمه details. وهذه طريقة أنيقة للتخلّص من خاصية دون تعديل الأصل: كل ما لم تسمّه ينتهي في الكائن الجديد.

صارت لدى Pocket بيانات واقعية. وفي الدرس التالي تلخّصها: مجاميع لكل فئة، وقوائم مرتّبة حسب المبلغ.

أهم النقاط

  • الكائن يجمع قيمًا مسمّاة (خصائص)؛ اقرأها بصيغة النقطة، أو بالأقواس المربّعة حين يكون اسم الخاصية في متغيّر.
  • الكائنات والمصفوفات تُتداوَل عبر المرجع (reference)، لذلك يمكن أن يشير اسمان إلى الكائن نفسه، ويظهر التغيير عبر أحدهما من خلال الآخر.
  • النشر ({ ...expense, amount: 500 } و[...list, item]) يصنع نسخة جديدة سطحية، وهكذا تحدّث البيانات دون تعديلها في مكانها.
  • التفكيك (const { label, amount } = expense) يسحب الخصائص إلى متغيّرات، ويعمل في معاملات الدوال مع القيم الافتراضية.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

ماذا يطبع هذا الكود؟

JavaScript
const a = { label: "Coffee", amount: 450 };
const b = a;
b.amount = 500;
console.log(a.amount);
السؤال 2 من 4

const updated = { ...expense, amount: 500 }; ما الصحيح بعد ذلك؟

السؤال 3 من 4

إذا كان const expense = { label: "Bus", amount: 270 };، فماذا يعطيك const { label, category = "other" } = expense;؟

السؤال 4 من 4

لديك const key = "category";. كيف تقرأ expense.category باستخدام key؟

تدريب

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

حدّث المصاريف دون تعديلها في مكانها

دوال كود البداية تغيّر المصفوفات والكائنات التي تُعطى لها، فتتغيّر بيانات المُستدعي من وراء ظهره. أعِد كتابتها بحيث تُرجع قيمًا جديدة بدلًا من ذلك:

  • addExpense(list, expense) تُرجع مصفوفة جديدة في نهايتها expense.
  • updateAmount(list, id, amount) تُرجع مصفوفة جديدة يكون فيها المصروف صاحب ذلك الـ id كائنًا جديدًا بالمبلغ الجديد amount. وتبقى المصاريف الأخرى كما هي، ولا يتغيّر أي شيء في list.
  • describe(expense) تُرجع نصًا مثل "Train: $12.20 (travel)"، مستخدمةً "other" حين لا توجد فئة. فكّك المعامل.
const expenses = [
  { id: "exp-1", label: "Coffee", amount: 450, category: "food" },
  { id: "exp-2", label: "Train", amount: 1220, category: "travel" },
];

// Return a NEW array with the expense added at the end
function addExpense(list, expense) {
  list.push(expense);
  return list;
}

// Return a NEW array where the expense with this id has the new amount
function updateAmount(list, id, amount) {
  const found = list.find((e) => e.id === id);
  found.amount = amount;
  return list;
}

// "Train: $12.20 (travel)"; category defaults to "other"
function describe(expense) {
  return expense.label;
}

console.log(describe(expenses[1]));
جارٍ تحميل المحرّر…

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

الـ console

(لا مخرجات)

الفحوص

  • addExpense تُرجع القائمة وفي نهايتها المصروف الجديد
  • addExpense تترك المصفوفة الأصلية دون تغيير
  • updateAmount تغيّر مبلغ المصروف المطابق
  • updateAmount تترك المصفوفة والكائنات الأصلية دون تغيير
  • describe تنسّق المصروف، وتجعل الفئة “other” افتراضيًا