الوحدة 3 · العمل مع البيانات
الكائنات والتفكيك (Destructuring) والنشر (Spread)
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تمثّل السجلّات الحقيقية على شكل كائنات، وتقرأ خصائصها وتحدّثها، وتفهم أن الكائنات تُتداوَل عبر المرجع، وتنسخها وتحدّثها وتفكّكها باستخدام الـ spread والـ destructuring.
- 1الكائنات: قيم مسمّاة مجتمعة
- 2المراجع: اسمان وكائن واحد
- 3النشر: انسخ وحدّث دون تعديل في المكان
- 4التفكيك: فكّ الحزمة إلى متغيّرات
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
حتى الآن كان المصروف في Pocket مجرّد رقم. لكن المصروف الحقيقي له عدّة حقائق مترابطة: ما هو، وكم كلّف، وما فئته، ومتى كان. وحفظها في مصفوفات منفصلة (labels[2] وamounts[2] وcategories[2]) ينجح إلى أن تُرتَّب إحدى المصفوفات دون الأخرى. تحتاج إلى قيمة واحدة تحملها كلها: الكائن (object).
الكائنات: قيم مسمّاة مجتمعة
يُكتب الكائن بأقواس معقوفة ويحتوي على خصائص (properties)، كل منها اسم وقيمة:
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، لا خطأ.
يمكنك إضافة الخصائص وتغييرها وحذفها بعد إنشاء الكائن:
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 }، وستراه باستمرار في الكود الحقيقي:
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، وكل ما تعلّمته في الدرس السابق ما زال ينطبق:
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) إليه، مثل العنوان. ونسخ المتغيّر ينسخ العنوان:
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=== مع الكائنات تقارن المراجع، لا المحتويات. فالكائنان اللذان يبدوان متطابقين يظلّان كائنين مختلفين.
ولهذا أهمية لأن الدوال تستلم المراجع أيضًا. فإن غيّرت دالة كائنًا أُعطي لها، تغيّر كائن المُستدعي:
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)، أي النقاط الثلاث، تنسخ كل خصائص كائن (أو عناصر مصفوفة) إلى كائن جديد:
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، هذه هي الطريقة المعتمدة لتغيير عنصر واحد في قائمة دون تعديل أي شيء في مكانه:
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) يسحب الخصائص من كائن إلى متغيّرات تحمل الأسماء نفسها:
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. ومع المصفوفات، يعتمد التفكيك على الموضع بدلًا من الاسم.
والتفكيك أكثر ما يكون نفعًا في معاملات الدوال، حيث يوثّق بالضبط الخصائص التي تحتاجها الدالة:
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) يسحب الخصائص إلى متغيّرات، ويعمل في معاملات الدوال مع القيم الافتراضية.